4365f78ef7
Co-authored-by: Cursor <cursoragent@cursor.com>
1404 lines
70 KiB
C#
1404 lines
70 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Numerics;
|
||
using System.Reflection;
|
||
using System.Runtime.CompilerServices;
|
||
using System.Runtime.InteropServices;
|
||
using Dalamud.Game.ClientState.Objects.Types;
|
||
using Dalamud.Game.Gui;
|
||
using HSUI.Config;
|
||
using HSUI.Enums;
|
||
using HSUI.Helpers;
|
||
using HSUI.Interface.Bars;
|
||
using Dalamud.Bindings.ImGui;
|
||
using Dalamud.Interface.Utility;
|
||
using Dalamud.Utility;
|
||
using Lumina.Excel;
|
||
using Lumina.Excel.Sheets;
|
||
using Lumina.Text.ReadOnly;
|
||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||
using LuminaAction = Lumina.Excel.Sheets.Action;
|
||
|
||
namespace HSUI.Interface.GeneralElements
|
||
{
|
||
public class ActionBarsHud : DraggableHudElement, IHudElementWithActor, IHudElementWithVisibilityConfig
|
||
{
|
||
private HotbarBarConfig Config => (HotbarBarConfig)_config;
|
||
|
||
private bool _wasDragging;
|
||
private uint _dragId;
|
||
private RaptureHotbarModule.HotbarSlotType _dragSlotType;
|
||
private int _pendingSlotIconIndex = -1;
|
||
private uint _pendingSlotIconId;
|
||
private int _pendingSlotIconFramesLeft;
|
||
/// <summary>When we process a game drop on a slot, we must suppress the InvisibleButton "click" that would trigger ExecuteSlot.</summary>
|
||
private int _lastFrameDroppedOnSlot = -1;
|
||
/// <summary>When we had a game drag and the user released, suppress all slot clicks this frame (release is not a click).</summary>
|
||
private bool _suppressSlotClicksAfterDragRelease;
|
||
|
||
private const string SlotPayloadType = "HSUI_HOTBAR_SLOT";
|
||
private const int SlotsPerBar = 12;
|
||
|
||
private static int _imGuiDragSourceSlotId = -1;
|
||
private static bool _anyHotbarAcceptedDrop;
|
||
private static int _lastOverlayFrame = -1;
|
||
/// <summary>Deferred clear when release-outside: set when no overlay accepted, executed next frame.</summary>
|
||
private static int _pendingReleaseOutsideSlotId = -1;
|
||
/// <summary>To avoid PICKUP log spam: only log when we first start dragging a new slot.</summary>
|
||
private static int _lastLoggedPickupSlotId = -1;
|
||
|
||
/// <summary>Encode hotbar (1-10) and slot (0-11) into internal slot id (0-119).</summary>
|
||
private static int ToSlotId(int hotbarIndex, int slotIndex)
|
||
{
|
||
int bar = Math.Clamp(hotbarIndex, 1, 10) - 1;
|
||
int slot = Math.Clamp(slotIndex, 0, 11);
|
||
return bar * SlotsPerBar + slot;
|
||
}
|
||
|
||
/// <summary>Decode internal slot id to hotbar (1-10) and slot (0-11). Returns (-1,-1) if invalid.</summary>
|
||
private static (int hotbarIndex, int slotIndex) FromSlotId(int slotId)
|
||
{
|
||
if (slotId < 0 || slotId >= 10 * SlotsPerBar) return (-1, -1);
|
||
return (slotId / SlotsPerBar + 1, slotId % SlotsPerBar);
|
||
}
|
||
|
||
[StructLayout(LayoutKind.Sequential)]
|
||
private struct SlotDragPayload
|
||
{
|
||
public int SlotId;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Visibility for HSUI Action Bars is driven by Visibility → Hotbars (Hotbar 1–10), not the per-element config.
|
||
/// </summary>
|
||
public VisibilityConfig VisibilityConfig => GetHotbarVisibilityConfig();
|
||
|
||
private VisibilityConfig GetHotbarVisibilityConfig()
|
||
{
|
||
var hotbars = ConfigurationManager.Instance?.GetConfigObject<HotbarsVisibilityConfig>();
|
||
if (hotbars == null) return Config.VisibilityConfig;
|
||
var list = hotbars.GetHotbarConfigs();
|
||
int idx = Config.HotbarIndex - 1;
|
||
if (idx < 0 || idx >= list.Count) return Config.VisibilityConfig;
|
||
return list[idx];
|
||
}
|
||
|
||
public IGameObject? Actor { get; set; }
|
||
|
||
public ActionBarsHud(HotbarBarConfig config, string displayName) : base(config, displayName) { }
|
||
|
||
protected override (List<Vector2>, List<Vector2>) ChildrenPositionsAndSizes()
|
||
{
|
||
var size = BarSize();
|
||
return (new List<Vector2> { Config.Position }, new List<Vector2> { size });
|
||
}
|
||
|
||
private Vector2 BarSize()
|
||
{
|
||
var (cols, rows) = Config.GetLayoutGrid();
|
||
int effectiveCols = Math.Min(cols, Config.SlotCount);
|
||
int effectiveRows = (Config.SlotCount + effectiveCols - 1) / effectiveCols;
|
||
float w = effectiveCols * Config.SlotSize.X + (effectiveCols - 1) * Config.SlotPadding;
|
||
float h = effectiveRows * Config.SlotSize.Y + (effectiveRows - 1) * Config.SlotPadding;
|
||
return new Vector2(w, h);
|
||
}
|
||
|
||
private Vector2 GetSlotPosition(int slotIndex, Vector2 slotSize, int pad)
|
||
{
|
||
var (cols, _) = Config.GetLayoutGrid();
|
||
int col = slotIndex % cols;
|
||
int row = slotIndex / cols;
|
||
return new Vector2(col * (slotSize.X + pad), row * (slotSize.Y + pad));
|
||
}
|
||
|
||
public override void DrawChildren(Vector2 origin)
|
||
{
|
||
if (!Config.Enabled || Actor == null)
|
||
return;
|
||
|
||
if (ActionBarsManager.Instance == null)
|
||
return;
|
||
|
||
_lastFrameDroppedOnSlot = -1;
|
||
_suppressSlotClicksAfterDragRelease = false;
|
||
|
||
var barSize = BarSize();
|
||
var topLeft = Utils.GetAnchoredPosition(origin + Config.Position, barSize, Config.Anchor);
|
||
var slotSize = Config.SlotSize;
|
||
int pad = Config.SlotPadding;
|
||
bool showCd = Config.ShowCooldownOverlay;
|
||
bool showCdNumbers = Config.ShowCooldownNumbers;
|
||
bool showBorder = Config.ShowBorder;
|
||
|
||
AddDrawAction(Config.StrataLevel, () =>
|
||
{
|
||
HandleDragDrop(topLeft, barSize, slotSize, pad);
|
||
// Refresh slot data after placement so overwrite shows correctly
|
||
var slots = ActionBarsManager.Instance.GetSlotData(Config.HotbarIndex, Config.SlotCount);
|
||
|
||
void DrawBarContent(ImDrawListPtr drawList)
|
||
{
|
||
using (FontsManager.Instance.PushFont(FontsConfig.DefaultSmallFontKey))
|
||
{
|
||
for (int i = 0; i < slots.Count; i++)
|
||
{
|
||
var slot = slots[i];
|
||
uint displayIconId = slot.IconId;
|
||
bool isEmpty = slot.IsEmpty;
|
||
bool usePending = _pendingSlotIconIndex == i && _pendingSlotIconId != 0;
|
||
if (usePending)
|
||
{
|
||
displayIconId = _pendingSlotIconId;
|
||
isEmpty = false;
|
||
}
|
||
|
||
var slotOffset = GetSlotPosition(i, slotSize, pad);
|
||
var pos = topLeft + slotOffset;
|
||
var size = new Vector2(slotSize.X, slotSize.Y);
|
||
bool hovered = !isEmpty && ImGui.IsMouseHoveringRect(pos, pos + size);
|
||
|
||
if (isEmpty)
|
||
{
|
||
drawList.AddRectFilled(pos, pos + size, 0x22000000);
|
||
if (showBorder)
|
||
drawList.AddRect(pos, pos + size, 0xFF444444);
|
||
}
|
||
else
|
||
{
|
||
uint color = hovered ? 0xFFFFFFFF : (slot.IsUsable ? 0xFFFFFFFF : 0xAA888888);
|
||
bool useHighlightBorder = hovered && showBorder;
|
||
bool isComboNext = Config.ShowComboHighlight && IsComboNextAction(slot);
|
||
DrawHelper.DrawIcon(displayIconId, pos, size, showBorder && !useHighlightBorder && !isComboNext, color, drawList);
|
||
|
||
if (useHighlightBorder)
|
||
drawList.AddRect(pos, pos + size, 0xFF88CCFF, 0, ImDrawFlags.None, 2);
|
||
else if (isComboNext)
|
||
{
|
||
var ch = Config.ComboHighlightConfig;
|
||
DrawHelper.DrawComboHighlightRect(drawList, pos, pos + size,
|
||
ch.Color.Base, ch.Thickness, ch.ShowGlow, ch.LineStyle);
|
||
}
|
||
|
||
if (showCd && slot.CooldownPercent > 0 && _pendingSlotIconIndex != i)
|
||
{
|
||
float total = 100f;
|
||
float elapsed = total - slot.CooldownPercent;
|
||
DrawHelper.DrawIconCooldown(pos, size, elapsed, total, drawList);
|
||
if (showCdNumbers && slot.CooldownSecondsLeft > 0)
|
||
{
|
||
string cdText = slot.CooldownSecondsLeft.ToString();
|
||
Vector2 textSize = ImGui.CalcTextSize(cdText);
|
||
Vector2 textPos = pos + (size - textSize) * 0.5f;
|
||
DrawHelper.DrawOutlinedText(cdText, textPos, 0xFFFFFFFF, 0xFF000000, drawList, 1);
|
||
}
|
||
}
|
||
|
||
if (Config.ShowKeypressFlash)
|
||
{
|
||
float flashAlpha = InputsHelper.GetKeypressFlashAlpha(Config.HotbarIndex, i, 0.2);
|
||
if (flashAlpha > 0.001f)
|
||
{
|
||
byte a = (byte)(flashAlpha * 0.5f * 255);
|
||
drawList.AddRectFilled(pos, pos + size, (uint)(a << 24 | 0xFFFFFF));
|
||
}
|
||
}
|
||
}
|
||
|
||
if (Config.ShowSlotNumbers && !slot.IsEmpty)
|
||
{
|
||
string label = !string.IsNullOrWhiteSpace(slot.KeybindHint)
|
||
? slot.KeybindHint
|
||
: ActionBarsManager.GetDefaultKeybindLabel(Config.HotbarIndex, i);
|
||
Vector2 labelSize = ImGui.CalcTextSize(label);
|
||
Vector2 labelPos = new Vector2(pos.X + size.X - labelSize.X - 2, pos.Y + size.Y - labelSize.Y - 2);
|
||
DrawHelper.DrawOutlinedText(label, labelPos, 0xFFFFFFFF, 0xFF000000, drawList, 1);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (IsGameDragging())
|
||
{
|
||
var mp = ImGui.GetMousePos();
|
||
float hole = 32f;
|
||
var cursorRect = new ClipRect(mp - new Vector2(hole), mp + new Vector2(hole));
|
||
var barRect = new ClipRect(topLeft, topLeft + barSize);
|
||
var inverted = ClipRectsHelper.GetInvertedClipRects(cursorRect);
|
||
var drawList = ImGui.GetWindowDrawList();
|
||
for (int i = 0; i < inverted.Length; i++)
|
||
{
|
||
var inter = barRect.Intersect(inverted[i]);
|
||
if (!inter.HasValue) continue;
|
||
var r = inter.Value;
|
||
ImGui.PushClipRect(r.Min, r.Max, false);
|
||
DrawBarContent(drawList);
|
||
ImGui.PopClipRect();
|
||
}
|
||
}
|
||
else
|
||
{
|
||
DrawHelper.DrawInWindow(ID + "_actionbars", topLeft, barSize, false, DrawBarContent);
|
||
}
|
||
});
|
||
|
||
// Overlay at HIGHEST so it captures input above the bar; required for icon swap.
|
||
// Use unique ID per hotbar to avoid any cross-bar conflicts (bar 2–10 had input capture issues).
|
||
AddDrawAction(StrataLevel.HIGHEST, () =>
|
||
{
|
||
var slots = ActionBarsManager.Instance.GetSlotData(Config.HotbarIndex, Config.SlotCount);
|
||
var flags = ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoBackground
|
||
| ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoSavedSettings
|
||
| ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoBringToFrontOnFocus
|
||
| ImGuiWindowFlags.NoNav;
|
||
|
||
ImGuiHelpers.ForceNextWindowMainViewport();
|
||
ImGui.SetNextWindowPos(topLeft);
|
||
ImGui.SetNextWindowSize(barSize);
|
||
string overlayId = $"HSUI_Hotbar{Config.HotbarIndex}_input";
|
||
if (!ImGui.Begin(overlayId, flags))
|
||
{
|
||
ImGui.End();
|
||
return;
|
||
}
|
||
|
||
int frame = ImGui.GetFrameCount();
|
||
if (frame != _lastOverlayFrame)
|
||
{
|
||
_lastOverlayFrame = frame;
|
||
_anyHotbarAcceptedDrop = false;
|
||
// Process deferred release-outside clear from previous frame (must run before overlays)
|
||
if (_pendingReleaseOutsideSlotId >= 0 && GetHotbarsConfig()?.GeneralOptions?.EnableReleaseOutsideToClear != false)
|
||
{
|
||
var (bar, slot) = FromSlotId(_pendingReleaseOutsideSlotId);
|
||
if (bar >= 1 && slot >= 0)
|
||
ActionBarsManager.Instance?.ClearSlot(bar, slot);
|
||
_pendingReleaseOutsideSlotId = -1;
|
||
}
|
||
}
|
||
|
||
bool acceptedDrop = false;
|
||
for (int i = 0; i < Config.SlotCount; i++)
|
||
{
|
||
var slotOffset = GetSlotPosition(i, slotSize, pad);
|
||
var slotPos = topLeft + slotOffset;
|
||
ImGui.SetCursorScreenPos(slotPos);
|
||
bool clicked = ImGui.InvisibleButton($"##{overlayId}_slot_{i}", slotSize);
|
||
bool isHovered = ImGui.IsItemHovered();
|
||
bool isActive = ImGui.IsItemActive();
|
||
|
||
bool shiftHeld = ImGui.GetIO().KeyShift;
|
||
|
||
int targetSlotId = ToSlotId(Config.HotbarIndex, i);
|
||
|
||
if (GetHotbarsConfig()?.GeneralOptions?.EnableShiftDragToRearrange != false && ImGui.BeginDragDropTarget())
|
||
{
|
||
var payload = ImGui.AcceptDragDropPayload(SlotPayloadType);
|
||
if (payload != ImGuiPayloadPtr.Null && payload.IsDataType(SlotPayloadType) && TryGetSlotPayload(payload, out var src))
|
||
{
|
||
acceptedDrop = true;
|
||
_anyHotbarAcceptedDrop = true;
|
||
_imGuiDragSourceSlotId = -1;
|
||
_pendingReleaseOutsideSlotId = -1;
|
||
_lastLoggedPickupSlotId = -1;
|
||
if (src.SlotId != targetSlotId)
|
||
{
|
||
var (srcBar, srcSlot) = FromSlotId(src.SlotId);
|
||
if (srcBar >= 1 && srcSlot >= 0)
|
||
{
|
||
ActionBarsManager.Instance?.SwapSlots(srcBar, srcSlot, Config.HotbarIndex, i,
|
||
Config.DebugDragDrop ? s => Plugin.Logger.Information($"[HSUI DragDrop DBG] {s}") : null);
|
||
}
|
||
}
|
||
}
|
||
ImGui.EndDragDropTarget();
|
||
}
|
||
|
||
if (GetHotbarsConfig()?.GeneralOptions?.EnableShiftDragToRearrange != false && !IsGameDragging() && shiftHeld && i < slots.Count && !slots[i].IsEmpty && isActive && ImGui.IsMouseDragging(ImGuiMouseButton.Left))
|
||
{
|
||
if (ImGui.BeginDragDropSource(ImGuiDragDropFlags.None))
|
||
{
|
||
_imGuiDragSourceSlotId = targetSlotId;
|
||
if (Config.DebugDragDrop && _lastLoggedPickupSlotId != targetSlotId)
|
||
{
|
||
_lastLoggedPickupSlotId = targetSlotId;
|
||
Plugin.Logger.Information($"[HSUI DragDrop DBG] Shift+drag PICKUP: bar={Config.HotbarIndex} slot={i} slotId={targetSlotId}");
|
||
}
|
||
SetSlotPayload(new SlotDragPayload { SlotId = targetSlotId });
|
||
var icon = TexturesHelper.GetTextureFromIconId(slots[i].IconId);
|
||
if (icon != null)
|
||
ImGui.Image(icon.Handle, new Vector2(32, 32));
|
||
ImGui.EndDragDropSource();
|
||
}
|
||
}
|
||
|
||
if (clicked && !acceptedDrop && !ImGui.IsMouseDragging(ImGuiMouseButton.Left) && i < slots.Count && !slots[i].IsEmpty)
|
||
{
|
||
if (_suppressSlotClicksAfterDragRelease || i == _lastFrameDroppedOnSlot)
|
||
{
|
||
if (Config.DebugDragDrop)
|
||
Plugin.Logger.Information($"[HSUI DragDrop DBG] Suppressed ExecuteSlot slot={i} suppressAfterDrag={_suppressSlotClicksAfterDragRelease} lastDroppedOn={_lastFrameDroppedOnSlot}");
|
||
_suppressSlotClicksAfterDragRelease = false;
|
||
_lastFrameDroppedOnSlot = -1;
|
||
}
|
||
else if (shiftHeld)
|
||
{
|
||
ActionBarsManager.Instance?.ClearSlot(Config.HotbarIndex, i);
|
||
}
|
||
else
|
||
{
|
||
if (Config.DebugDragDrop)
|
||
Plugin.Logger.Information($"[HSUI DragDrop DBG] ExecuteSlot hotbar={Config.HotbarIndex} slot={i}");
|
||
ActionBarsManager.Instance?.ExecuteSlot(Config.HotbarIndex, i);
|
||
}
|
||
}
|
||
|
||
if (Config.ShowTooltips && isHovered && i < slots.Count)
|
||
{
|
||
var slot = slots[i];
|
||
if (!slot.IsEmpty)
|
||
{
|
||
var (title, text) = GetSlotTooltip(slot);
|
||
if (!string.IsNullOrEmpty(title) || !string.IsNullOrEmpty(text))
|
||
{
|
||
string body = string.IsNullOrEmpty(text) ? title : text;
|
||
TooltipsHelper.Instance.ShowTooltipOnCursor(body, title, slot.ActionId, "");
|
||
if (IsTooltipDebugEnabled())
|
||
Plugin.Logger.Information($"[HSUI Tooltip DBG] ActionBar tooltip (main overlay): slot={i} title='{title}'");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Release-outside clear: defer to next frame so we run after all overlays have had a chance to accept
|
||
if (GetHotbarsConfig()?.GeneralOptions?.EnableReleaseOutsideToClear != false && ImGui.GetIO().KeyShift && _imGuiDragSourceSlotId >= 0 &&
|
||
ImGui.IsMouseReleased(ImGuiMouseButton.Left) && !_anyHotbarAcceptedDrop)
|
||
{
|
||
if (Config.DebugDragDrop)
|
||
{
|
||
var (bar, slot) = FromSlotId(_imGuiDragSourceSlotId);
|
||
Plugin.Logger.Information($"[HSUI DragDrop DBG] Release outside: defer clear bar={bar} slot={slot}");
|
||
}
|
||
_pendingReleaseOutsideSlotId = _imGuiDragSourceSlotId;
|
||
_imGuiDragSourceSlotId = -1;
|
||
_lastLoggedPickupSlotId = -1;
|
||
}
|
||
|
||
ImGui.End();
|
||
});
|
||
}
|
||
|
||
/// <summary>Tooltip-only overlay when click overlay is skipped (proxy on / HUD not locked). Uses NoInputs so it doesn't capture clicks.</summary>
|
||
private void DrawTooltipOnlyOverlay(Vector2 topLeft, Vector2 barSize, Vector2 slotSize, int pad)
|
||
{
|
||
if (!Config.ShowTooltips)
|
||
return;
|
||
var slots = ActionBarsManager.Instance?.GetSlotData(Config.HotbarIndex, Config.SlotCount);
|
||
if (slots == null)
|
||
return;
|
||
var flags = ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoBackground
|
||
| ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoSavedSettings
|
||
| ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoBringToFrontOnFocus
|
||
| ImGuiWindowFlags.NoInputs;
|
||
ImGuiHelpers.ForceNextWindowMainViewport();
|
||
ImGui.SetNextWindowPos(topLeft);
|
||
ImGui.SetNextWindowSize(barSize);
|
||
if (!ImGui.Begin(ID + "_actionbars_tooltip_only", flags))
|
||
{
|
||
ImGui.End();
|
||
return;
|
||
}
|
||
for (int i = 0; i < Config.SlotCount && i < slots.Count; i++)
|
||
{
|
||
var slotOffset = GetSlotPosition(i, slotSize, pad);
|
||
var slotPos = topLeft + slotOffset;
|
||
if (ImGui.IsMouseHoveringRect(slotPos, slotPos + slotSize))
|
||
{
|
||
var slot = slots[i];
|
||
if (!slot.IsEmpty)
|
||
{
|
||
var (title, text) = GetSlotTooltip(slot);
|
||
if (!string.IsNullOrEmpty(title) || !string.IsNullOrEmpty(text))
|
||
{
|
||
string body = string.IsNullOrEmpty(text) ? title : text;
|
||
TooltipsHelper.Instance.ShowTooltipOnCursor(body, title, slot.ActionId, "");
|
||
if (IsTooltipDebugEnabled())
|
||
Plugin.Logger.Information($"[HSUI Tooltip DBG] ActionBar tooltip (overlay): slot={i} title='{title}'");
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
ImGui.End();
|
||
}
|
||
|
||
private static bool IsTooltipDebugEnabled()
|
||
{
|
||
try
|
||
{
|
||
return ConfigurationManager.Instance?.GetConfigObject<TooltipsConfig>()?.DebugTooltips == true;
|
||
}
|
||
catch { return false; }
|
||
}
|
||
|
||
private static (uint LastActionId, string? LastDesc, double LastTime) _tooltipDebugLogState;
|
||
private static bool ShouldLogTooltipDebug(uint actionId, string name, string? desc)
|
||
{
|
||
double now = ImGui.GetTime();
|
||
var (lastId, lastDesc, lastTime) = _tooltipDebugLogState;
|
||
if (actionId == lastId && desc == lastDesc && now - lastTime < 1f)
|
||
return false;
|
||
_tooltipDebugLogState = (actionId, desc, now);
|
||
return true;
|
||
}
|
||
|
||
private static bool TryGetDebug()
|
||
{
|
||
try
|
||
{
|
||
var configs = ConfigurationManager.Instance?.GetObjects<HotbarBarConfig>();
|
||
return configs != null && configs.Exists(c => c.DebugDragDrop);
|
||
}
|
||
catch { return false; }
|
||
}
|
||
|
||
private static (int Int1, int Int2, double LastTime) _lastItemPayloadLog;
|
||
private static bool ShouldLogItemPayload(int int1, int int2)
|
||
{
|
||
double now = ImGui.GetTime();
|
||
var last = _lastItemPayloadLog;
|
||
if (int1 == last.Int1 && int2 == last.Int2 && now - last.LastTime < 0.5)
|
||
return false;
|
||
_lastItemPayloadLog = (int1, int2, now);
|
||
return true;
|
||
}
|
||
|
||
private static (int Int1, int Int2, double LastTime) _lastMacroPayloadLog;
|
||
private static bool ShouldLogMacroPayload(int rawInt1, int rawInt2)
|
||
{
|
||
double now = ImGui.GetTime();
|
||
var last = _lastMacroPayloadLog;
|
||
if (rawInt1 == last.Int1 && rawInt2 == last.Int2 && now - last.LastTime < 2.0)
|
||
return false;
|
||
_lastMacroPayloadLog = (rawInt1, rawInt2, now);
|
||
return true;
|
||
}
|
||
|
||
private static HotbarsConfig? GetHotbarsConfig() =>
|
||
ConfigurationManager.Instance?.GetConfigObject<HotbarsConfig>();
|
||
|
||
private static (int Type, int Int1, int Int2, double LastTime) _lastDragNoPayloadLog;
|
||
private static unsafe bool ShouldLogDragNoPayload(AtkDragDropManager* dm)
|
||
{
|
||
if (dm == null) return false;
|
||
var dd = dm->DragDrop1;
|
||
int type = dd != null ? (int)dd->DragDropType : 0;
|
||
int i1 = dm->PayloadContainer.Int1;
|
||
int i2 = dm->PayloadContainer.Int2;
|
||
double now = ImGui.GetTime();
|
||
var last = _lastDragNoPayloadLog;
|
||
if (type == last.Type && i1 == last.Int1 && i2 == last.Int2 && now - last.LastTime < 0.5)
|
||
return false;
|
||
_lastDragNoPayloadLog = (type, i1, i2, now);
|
||
return true;
|
||
}
|
||
|
||
private static unsafe bool IsGameDragging()
|
||
{
|
||
var stage = AtkStage.Instance();
|
||
if (stage == null) return false;
|
||
var dm = (AtkDragDropManager*)Unsafe.AsPointer(ref stage->DragDropManager);
|
||
return dm->IsDragging;
|
||
}
|
||
|
||
private static unsafe bool TryGetSlotPayload(ImGuiPayloadPtr payload, out SlotDragPayload src)
|
||
{
|
||
if (payload.Data == null || payload.DataSize < sizeof(SlotDragPayload))
|
||
{
|
||
src = default;
|
||
return false;
|
||
}
|
||
src = *(SlotDragPayload*)payload.Data;
|
||
return true;
|
||
}
|
||
|
||
private static void SetSlotPayload(SlotDragPayload pl)
|
||
{
|
||
var type = new ImU8String(SlotPayloadType);
|
||
var data = MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref pl, 1));
|
||
ImGui.SetDragDropPayload(type, data, ImGuiCond.None);
|
||
type.Recycle();
|
||
}
|
||
|
||
private static bool IsComboNextAction(ActionBarsManager.SlotInfo slot)
|
||
{
|
||
if (slot.IsEmpty || slot.ActionId == 0) return false;
|
||
try
|
||
{
|
||
var slotType = slot.SlotType;
|
||
var actionType = slotType switch
|
||
{
|
||
RaptureHotbarModule.HotbarSlotType.Action => ActionType.Action,
|
||
RaptureHotbarModule.HotbarSlotType.GeneralAction => ActionType.GeneralAction,
|
||
RaptureHotbarModule.HotbarSlotType.CraftAction => ActionType.CraftAction,
|
||
RaptureHotbarModule.HotbarSlotType.PetAction => ActionType.PetAction,
|
||
_ => (ActionType?)null
|
||
};
|
||
if (!actionType.HasValue) return false;
|
||
return SpellHelper.Instance.IsActionHighlighted(slot.ActionId, actionType.Value);
|
||
}
|
||
catch { return false; }
|
||
}
|
||
|
||
private unsafe void HandleDragDrop(Vector2 topLeft, Vector2 barSize, Vector2 slotSize, int pad)
|
||
{
|
||
bool debug = Config.DebugDragDrop;
|
||
|
||
// NOTE: Release-outside clear is done in the overlay only. HandleDragDrop runs before overlay
|
||
// processes AcceptDragDropPayload, so we must not clear here or we'd clear the source before
|
||
// the drop is processed (breaking swap/move when dropping on a slot).
|
||
|
||
if (_pendingSlotIconIndex >= 0 && _pendingSlotIconFramesLeft > 0)
|
||
{
|
||
_pendingSlotIconFramesLeft--;
|
||
if (_pendingSlotIconFramesLeft <= 0)
|
||
_pendingSlotIconIndex = -1;
|
||
}
|
||
else if (_pendingSlotIconIndex >= 0)
|
||
{
|
||
_pendingSlotIconIndex = -1;
|
||
}
|
||
|
||
var stage = AtkStage.Instance();
|
||
if (stage == null) return;
|
||
var dm = (AtkDragDropManager*)Unsafe.AsPointer(ref stage->DragDropManager);
|
||
bool dragging = dm->IsDragging;
|
||
|
||
if (dragging)
|
||
{
|
||
if (GetHotbarsConfig()?.GeneralOptions?.EnableDragDropFromGame != false && TryGetDragPayload(out var slotType, out var id))
|
||
{
|
||
_dragSlotType = slotType;
|
||
_dragId = id;
|
||
if (!_wasDragging && debug)
|
||
Plugin.Logger.Information($"[HSUI DragDrop] Game drag detected, payload: type={slotType} id={id}");
|
||
_wasDragging = true;
|
||
|
||
var mp = ImGui.GetMousePos();
|
||
int idx = GetSlotIndexAtPosition(topLeft, barSize, slotSize, pad, Config.SlotCount, Config.GetLayoutGrid().Cols, mp, false);
|
||
if (idx >= 0)
|
||
{
|
||
InputsHelper.SuppressExecuteSlotByIdAfterDrop((uint)(Config.HotbarIndex - 1), (uint)idx, 500);
|
||
}
|
||
|
||
if (ImGui.IsMouseReleased(ImGuiMouseButton.Left))
|
||
{
|
||
int releaseIdx = GetSlotIndexAtPosition(topLeft, barSize, slotSize, pad, Config.SlotCount, Config.GetLayoutGrid().Cols, mp, debug);
|
||
if (releaseIdx >= 0 && slotType != RaptureHotbarModule.HotbarSlotType.Empty && (id != 0 || slotType == RaptureHotbarModule.HotbarSlotType.Macro))
|
||
{
|
||
if (!ImGui.GetIO().KeyShift)
|
||
{
|
||
try
|
||
{
|
||
var module = RaptureHotbarModule.Instance();
|
||
if (module != null && module->ModuleReady)
|
||
{
|
||
uint barId = (uint)(Config.HotbarIndex - 1);
|
||
ref var displayBar = ref module->StandardHotbars[(int)barId];
|
||
var displaySlot = displayBar.GetHotbarSlot((uint)releaseIdx);
|
||
if (displaySlot != null)
|
||
{
|
||
displaySlot->Set(slotType, id);
|
||
displaySlot->LoadIconId();
|
||
}
|
||
module->SetAndSaveSlot(barId, (uint)releaseIdx, slotType, id);
|
||
uint iconId = GetIconIdForPayload(slotType, id);
|
||
if (iconId == 0)
|
||
{
|
||
var bar = module->StandardHotbars[(int)barId];
|
||
var writtenSlot = bar.GetHotbarSlot((uint)releaseIdx);
|
||
if (writtenSlot != null)
|
||
iconId = (uint)writtenSlot->IconId;
|
||
}
|
||
if (iconId != 0)
|
||
{
|
||
_pendingSlotIconIndex = releaseIdx;
|
||
_pendingSlotIconId = iconId;
|
||
_pendingSlotIconFramesLeft = 300;
|
||
}
|
||
_lastFrameDroppedOnSlot = releaseIdx;
|
||
InputsHelper.SuppressExecuteSlotByIdAfterDrop(barId, (uint)releaseIdx, 300);
|
||
if (slotType == RaptureHotbarModule.HotbarSlotType.Action ||
|
||
slotType == RaptureHotbarModule.HotbarSlotType.GeneralAction ||
|
||
slotType == RaptureHotbarModule.HotbarSlotType.CraftAction ||
|
||
slotType == RaptureHotbarModule.HotbarSlotType.PetAction)
|
||
InputsHelper.SuppressUseActionAfterDrop(id, 300);
|
||
if (debug)
|
||
Plugin.Logger.Information($"[HSUI DragDrop] Placed {slotType} id={id} on slot {releaseIdx}");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Plugin.Logger.Warning($"[HSUI DragDrop] Drop failed: {ex.Message}");
|
||
}
|
||
}
|
||
try { dm->CancelDragDrop(true, true); }
|
||
catch { }
|
||
}
|
||
else
|
||
{
|
||
try { dm->CancelDragDrop(true, true); }
|
||
catch { }
|
||
}
|
||
_wasDragging = false;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
_dragSlotType = RaptureHotbarModule.HotbarSlotType.Empty;
|
||
_dragId = 0;
|
||
_wasDragging = true;
|
||
if (ImGui.IsMouseReleased(ImGuiMouseButton.Left))
|
||
{
|
||
try { dm->CancelDragDrop(true, true); }
|
||
catch { }
|
||
_wasDragging = false;
|
||
}
|
||
if (debug && ShouldLogDragNoPayload(dm))
|
||
{
|
||
try
|
||
{
|
||
var dd = dm->DragDrop1;
|
||
if (dd != null)
|
||
Plugin.Logger.Information($"[HSUI DragDrop DBG] Drag but no payload (clearing cache): DragDropType={dd->DragDropType} Int1={dm->PayloadContainer.Int1} Int2={dm->PayloadContainer.Int2} HoveredItem={Plugin.GameGui?.HoveredItem ?? 0}");
|
||
}
|
||
catch { }
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (_wasDragging)
|
||
{
|
||
_suppressSlotClicksAfterDragRelease = true;
|
||
var mp = ImGui.GetMousePos();
|
||
int idx = GetSlotIndexAtPosition(topLeft, barSize, slotSize, pad, Config.SlotCount, Config.GetLayoutGrid().Cols, mp, debug);
|
||
|
||
if (idx >= 0)
|
||
{
|
||
var slotType = _dragSlotType;
|
||
var id = _dragId;
|
||
// Only use payload we captured during the drag. Do NOT fall back to TryGetDragPayload here:
|
||
// after we eat LBUTTONUP, the game drag is cancelled and HoveredAction/HoveredItem can be
|
||
// stale (e.g. from macro menu UI), causing wrong placements (macro drag -> GeneralAction placed).
|
||
// If we never got valid payload (e.g. Macro Int2=0), skip the place.
|
||
bool hasValidPayload = slotType != RaptureHotbarModule.HotbarSlotType.Empty &&
|
||
(id != 0 || slotType == RaptureHotbarModule.HotbarSlotType.Macro);
|
||
|
||
if (hasValidPayload)
|
||
{
|
||
bool shiftHeld = ImGui.GetIO().KeyShift;
|
||
if (shiftHeld)
|
||
{
|
||
try { dm->CancelDragDrop(true, true); }
|
||
catch (Exception ex) { Plugin.Logger.Warning($"[HSUI DragDrop] CancelDragDrop: {ex.Message}"); }
|
||
ActionBarsManager.Instance?.ClearSlot(Config.HotbarIndex, idx);
|
||
if (debug)
|
||
Plugin.Logger.Information($"[HSUI DragDrop] Shift+release: cleared slot {idx}");
|
||
_lastFrameDroppedOnSlot = idx;
|
||
_wasDragging = false;
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
var module = RaptureHotbarModule.Instance();
|
||
if (module != null && module->ModuleReady)
|
||
{
|
||
uint barId = (uint)(Config.HotbarIndex - 1);
|
||
ref var displayBar = ref module->StandardHotbars[(int)barId];
|
||
var displaySlot = displayBar.GetHotbarSlot((uint)idx);
|
||
if (displaySlot != null)
|
||
{
|
||
displaySlot->Set(slotType, id);
|
||
displaySlot->LoadIconId();
|
||
}
|
||
module->SetAndSaveSlot(barId, (uint)idx, slotType, id);
|
||
try { dm->CancelDragDrop(true, true); }
|
||
catch (Exception ex2) { Plugin.Logger.Warning($"[HSUI DragDrop] CancelDragDrop: {ex2.Message}"); }
|
||
|
||
uint iconId = GetIconIdForPayload(slotType, id);
|
||
if (iconId == 0)
|
||
{
|
||
var bar = module->StandardHotbars[(int)barId];
|
||
var writtenSlot = bar.GetHotbarSlot((uint)idx);
|
||
if (writtenSlot != null)
|
||
iconId = (uint)writtenSlot->IconId;
|
||
}
|
||
if (iconId != 0)
|
||
{
|
||
_pendingSlotIconIndex = idx;
|
||
_pendingSlotIconId = iconId;
|
||
_pendingSlotIconFramesLeft = 300;
|
||
}
|
||
_lastFrameDroppedOnSlot = idx;
|
||
InputsHelper.SuppressExecuteSlotByIdAfterDrop(barId, (uint)idx, 300);
|
||
if (slotType == RaptureHotbarModule.HotbarSlotType.Action ||
|
||
slotType == RaptureHotbarModule.HotbarSlotType.GeneralAction ||
|
||
slotType == RaptureHotbarModule.HotbarSlotType.CraftAction ||
|
||
slotType == RaptureHotbarModule.HotbarSlotType.PetAction)
|
||
InputsHelper.SuppressUseActionAfterDrop(id, 300);
|
||
if (debug)
|
||
Plugin.Logger.Information($"[HSUI DragDrop] Placed {slotType} id={id} on slot {idx}");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Plugin.Logger.Warning($"[HSUI DragDrop] Drop processing failed: {ex.Message}\n{ex.StackTrace}");
|
||
}
|
||
}
|
||
else if (debug)
|
||
{
|
||
Plugin.Logger.Information($"[HSUI DragDrop] Release over slot {idx} but invalid payload: type={slotType} id={id}");
|
||
}
|
||
}
|
||
else if (debug)
|
||
{
|
||
Plugin.Logger.Information($"[HSUI DragDrop] Release outside bar area (idx={idx})");
|
||
}
|
||
_wasDragging = false;
|
||
}
|
||
else if (!dragging)
|
||
{
|
||
_wasDragging = false;
|
||
}
|
||
}
|
||
|
||
private const float DropMargin = 24f;
|
||
|
||
private static int GetSlotIndexAtPosition(Vector2 topLeft, Vector2 barSize, Vector2 slotSize, int pad, int slotCount, int cols, Vector2 pos, bool debug = false)
|
||
{
|
||
// topLeft and InvisibleButtons use the same coordinate system as ImGui.SetCursorScreenPos/GetMousePos.
|
||
// Use strict bar bounds (no extended reach) so only one hotbar matches when bars are stacked.
|
||
var viewport = ImGui.GetMainViewport();
|
||
Vector2 adjPos = pos - viewport.Pos;
|
||
|
||
int TryHit(Vector2 p)
|
||
{
|
||
float barMinX = topLeft.X;
|
||
float barMaxX = topLeft.X + barSize.X;
|
||
float barMinY = topLeft.Y;
|
||
float barMaxY = topLeft.Y + barSize.Y;
|
||
if (p.X < barMinX || p.X >= barMaxX || p.Y < barMinY || p.Y >= barMaxY)
|
||
return -1;
|
||
int best = -1;
|
||
float bestDistSq = float.MaxValue;
|
||
for (int i = 0; i < slotCount; i++)
|
||
{
|
||
int col = i % cols;
|
||
int row = i / cols;
|
||
float cx = topLeft.X + col * (slotSize.X + pad) + slotSize.X * 0.5f;
|
||
float cy = topLeft.Y + row * (slotSize.Y + pad) + slotSize.Y * 0.5f;
|
||
float dx = p.X - cx, dy = p.Y - cy;
|
||
float distSq = dx * dx + dy * dy;
|
||
float slotHalfW = slotSize.X * 0.5f + DropMargin;
|
||
float slotHalfH = slotSize.Y * 0.5f + DropMargin;
|
||
bool inSlot = System.Math.Abs(dx) <= slotHalfW && System.Math.Abs(dy) <= slotHalfH;
|
||
if (inSlot && distSq < bestDistSq) { bestDistSq = distSq; best = i; }
|
||
}
|
||
return best;
|
||
}
|
||
|
||
int idx = TryHit(pos);
|
||
if (idx < 0 && adjPos != pos)
|
||
idx = TryHit(adjPos);
|
||
|
||
if (debug)
|
||
Plugin.Logger.Information($"[HSUI DragDrop DBG] HitTest: rawMouse=({pos.X:F1},{pos.Y:F1}) viewport.Pos=({viewport.Pos.X:F1},{viewport.Pos.Y:F1}) adjPos=({adjPos.X:F1},{adjPos.Y:F1}) slotIdx={idx}");
|
||
return idx;
|
||
}
|
||
|
||
private static unsafe uint GetActiveHotbarClassJobId()
|
||
{
|
||
var module = RaptureHotbarModule.Instance();
|
||
if (module != null)
|
||
return module->ActiveHotbarClassJobId;
|
||
var player = Plugin.ObjectTable?.LocalPlayer;
|
||
return player != null ? (uint)player.ClassJob.RowId : 0;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gets drag payload from game/Dalamud. When game reports Item drag, prioritize HoveredItem
|
||
/// (HoveredAction can be stale from previous hotbar hover). Preserves full item id including HQ flag.
|
||
/// </summary>
|
||
private static unsafe bool TryGetDragPayload(out RaptureHotbarModule.HotbarSlotType slotType, out uint id)
|
||
{
|
||
slotType = RaptureHotbarModule.HotbarSlotType.Empty;
|
||
id = 0;
|
||
DragDropType gameType = DragDropType.Nothing;
|
||
try
|
||
{
|
||
var stage = AtkStage.Instance();
|
||
if (stage != null)
|
||
{
|
||
var dm = (AtkDragDropManager*)Unsafe.AsPointer(ref stage->DragDropManager);
|
||
var dd = dm->DragDrop1;
|
||
if (dd != null)
|
||
gameType = dd->DragDropType;
|
||
}
|
||
}
|
||
catch { }
|
||
|
||
bool isItemDrag = gameType is DragDropType.Item or DragDropType.Inventory_Item or DragDropType.RemoteInventory_Item
|
||
or DragDropType.ActionBar_Item or DragDropType.LetterEditor_Item;
|
||
|
||
bool isMacroDrag = gameType is DragDropType.Macro or DragDropType.ActionBar_Macro;
|
||
if (isMacroDrag)
|
||
{
|
||
try
|
||
{
|
||
var stage = AtkStage.Instance();
|
||
if (stage != null)
|
||
{
|
||
var dm = (AtkDragDropManager*)Unsafe.AsPointer(ref stage->DragDropManager);
|
||
int rawInt1 = dm->PayloadContainer.Int1;
|
||
int rawInt2 = dm->PayloadContainer.Int2;
|
||
// Int1=1 or 49 -> Individual (set 0), Int1=48 -> Shared (set 1). Int2 = 1-based macro index (1=first).
|
||
// CommandId: 1–100 Individual, 101–200 Shared. RaptureMacroModule set 0=Individual, set 1=Shared.
|
||
int set = (rawInt1 == 48) ? 1 : 0; // 48=Shared, else (1,49,...)=Individual
|
||
id = (uint)((set * 100) + rawInt2); // Int2 1-based: 1->id1, 2->id2, 3->id3
|
||
slotType = RaptureHotbarModule.HotbarSlotType.Macro;
|
||
if (TryGetDebug() && ShouldLogMacroPayload(rawInt1, rawInt2))
|
||
Plugin.Logger.Information($"[HSUI DragDrop DBG] Macro payload: Int1={rawInt1} Int2={rawInt2} -> set={set} idx={rawInt2} id={id}");
|
||
return id is >= 1 and <= 200;
|
||
}
|
||
}
|
||
catch { }
|
||
return false;
|
||
}
|
||
|
||
if (isItemDrag)
|
||
{
|
||
var hi = Plugin.GameGui?.HoveredItem ?? 0;
|
||
if (hi != 0)
|
||
{
|
||
id = (uint)(hi & 0xFFFFFFFF);
|
||
slotType = RaptureHotbarModule.HotbarSlotType.Item;
|
||
return true;
|
||
}
|
||
// HoveredItem can be 0 when game clears it; fallback: resolve from PayloadContainer (Int1=container, Int2=slot)
|
||
if (TryGetItemIdFromInventoryPayload(out id, TryGetDebug()))
|
||
{
|
||
slotType = RaptureHotbarModule.HotbarSlotType.Item;
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
if (TryGetDragPayloadFromGame(out slotType, out id))
|
||
return true;
|
||
return TryGetDragPayloadFromDalamud(out slotType, out id);
|
||
}
|
||
|
||
/// <summary>Map game PayloadContainer Int1 (agent/UI container id) to InventoryType.</summary>
|
||
private static InventoryType ContainerIdToInventoryType(int containerId)
|
||
{
|
||
return containerId switch
|
||
{
|
||
4 => InventoryType.EquippedItems,
|
||
7 => InventoryType.KeyItems,
|
||
48 => InventoryType.Inventory1,
|
||
49 => InventoryType.Inventory2,
|
||
50 => InventoryType.Inventory3,
|
||
51 => InventoryType.Inventory4,
|
||
52 => InventoryType.RetainerPage1,
|
||
53 => InventoryType.RetainerPage2,
|
||
54 => InventoryType.RetainerPage3,
|
||
55 => InventoryType.RetainerPage4,
|
||
56 => InventoryType.RetainerPage5,
|
||
57 => InventoryType.ArmoryMainHand,
|
||
58 => InventoryType.ArmoryHead,
|
||
59 => InventoryType.ArmoryBody,
|
||
60 => InventoryType.ArmoryHands,
|
||
61 => InventoryType.ArmoryLegs,
|
||
62 => InventoryType.ArmoryFeets,
|
||
63 => InventoryType.ArmoryOffHand,
|
||
64 => InventoryType.ArmoryEar,
|
||
65 => InventoryType.ArmoryNeck,
|
||
66 => InventoryType.ArmoryWrist,
|
||
67 => InventoryType.ArmoryRings,
|
||
68 => InventoryType.ArmorySoulCrystal,
|
||
69 => InventoryType.SaddleBag1,
|
||
70 => InventoryType.SaddleBag2,
|
||
71 => InventoryType.PremiumSaddleBag1,
|
||
72 => InventoryType.PremiumSaddleBag2,
|
||
_ => InventoryType.Invalid
|
||
};
|
||
}
|
||
|
||
/// <summary>Resolve visual (containerId, slotIndex) to real (invType, slot) for main inventory via ItemOrderModule.</summary>
|
||
private static unsafe bool TryResolveToRealSlot(int containerId, int visualSlot, out InventoryType realInvType, out int realSlot)
|
||
{
|
||
realInvType = InventoryType.Invalid;
|
||
realSlot = -1;
|
||
try
|
||
{
|
||
var iom = ItemOrderModule.Instance();
|
||
if (iom == null) return false;
|
||
var sorter = iom->InventorySorter;
|
||
if (sorter == null) return false;
|
||
int itemsPerPage = sorter->ItemsPerPage > 0 ? sorter->ItemsPerPage : 35;
|
||
int startIndex = containerId switch { 48 => 0, 49 => itemsPerPage, 50 => itemsPerPage * 2, 51 => itemsPerPage * 3, _ => -1 };
|
||
if (startIndex < 0) return false;
|
||
int displayIndex = startIndex + visualSlot;
|
||
if (displayIndex < 0 || displayIndex >= sorter->Items.LongCount) return false;
|
||
var entry = sorter->Items[displayIndex].Value;
|
||
if (entry == null) return false;
|
||
realInvType = InventoryType.Inventory1 + entry->Page;
|
||
realSlot = entry->Slot;
|
||
return true;
|
||
}
|
||
catch { return false; }
|
||
}
|
||
|
||
/// <summary>Resolve item ID from inventory when HoveredItem is 0. PayloadContainer Int1=container, Int2=slot.
|
||
/// Resolves through ItemOrderModule for main inventory (sorted bags) so we get the correct item.</summary>
|
||
private static unsafe bool TryGetItemIdFromInventoryPayload(out uint itemId, bool debug = false)
|
||
{
|
||
itemId = 0;
|
||
try
|
||
{
|
||
var stage = AtkStage.Instance();
|
||
if (stage == null) return false;
|
||
var dm = (AtkDragDropManager*)Unsafe.AsPointer(ref stage->DragDropManager);
|
||
int containerId = dm->PayloadContainer.Int1;
|
||
int slotIndex = dm->PayloadContainer.Int2;
|
||
InventoryType invType;
|
||
int slot;
|
||
if (containerId is 48 or 49 or 50 or 51 && TryResolveToRealSlot(containerId, slotIndex, out invType, out slot))
|
||
{
|
||
if (debug && ShouldLogItemPayload(containerId, slotIndex))
|
||
Plugin.Logger.Information($"[HSUI DragDrop DBG] Item payload: Int1={containerId} Int2={slotIndex} -> resolved to {invType} slot={slot}");
|
||
}
|
||
else
|
||
{
|
||
invType = ContainerIdToInventoryType(containerId);
|
||
slot = slotIndex;
|
||
if (invType == InventoryType.Invalid) return false;
|
||
}
|
||
var inv = InventoryManager.Instance();
|
||
if (inv == null) return false;
|
||
var item = inv->GetInventorySlot(invType, slot);
|
||
if (item == null || item->IsEmpty()) return false;
|
||
itemId = item->GetItemId();
|
||
if (debug && itemId != 0 && ShouldLogItemPayload(containerId, slotIndex))
|
||
Plugin.Logger.Information($"[HSUI DragDrop DBG] Item from inventory: Int1={containerId} Int2={slotIndex} -> {invType} slot={slot} itemId={itemId}");
|
||
return itemId != 0;
|
||
}
|
||
catch { return false; }
|
||
}
|
||
|
||
private static unsafe bool TryGetDragPayloadFromGame(out RaptureHotbarModule.HotbarSlotType slotType, out uint id)
|
||
{
|
||
slotType = RaptureHotbarModule.HotbarSlotType.Empty;
|
||
id = 0;
|
||
try
|
||
{
|
||
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;
|
||
slotType = UIGlobals.GetHotbarSlotTypeFromDragDropType(dd->DragDropType);
|
||
id = (uint)dm->PayloadContainer.Int2;
|
||
return slotType != RaptureHotbarModule.HotbarSlotType.Empty;
|
||
}
|
||
catch { return false; }
|
||
}
|
||
|
||
private static bool TryGetDragPayloadFromDalamud(out RaptureHotbarModule.HotbarSlotType slotType, out uint id)
|
||
{
|
||
slotType = RaptureHotbarModule.HotbarSlotType.Empty;
|
||
id = 0;
|
||
var ha = Plugin.GameGui?.HoveredAction;
|
||
if (ha != null && ha.ActionKind != HoverActionKind.None && ha.ActionID != 0)
|
||
{
|
||
slotType = GetHotbarSlotTypeFromHoverActionKind(ha.ActionKind);
|
||
if (slotType != RaptureHotbarModule.HotbarSlotType.Empty)
|
||
{
|
||
id = ha.ActionID;
|
||
return true;
|
||
}
|
||
}
|
||
var hi = Plugin.GameGui?.HoveredItem ?? 0;
|
||
if (hi != 0)
|
||
{
|
||
id = (uint)(hi & 0xFFFFFFFF);
|
||
slotType = RaptureHotbarModule.HotbarSlotType.Item;
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
private static RaptureHotbarModule.HotbarSlotType GetHotbarSlotTypeFromHoverActionKind(HoverActionKind kind)
|
||
{
|
||
return kind switch
|
||
{
|
||
HoverActionKind.Action => RaptureHotbarModule.HotbarSlotType.Action,
|
||
HoverActionKind.CraftingAction => RaptureHotbarModule.HotbarSlotType.CraftAction,
|
||
HoverActionKind.GeneralAction => RaptureHotbarModule.HotbarSlotType.GeneralAction,
|
||
HoverActionKind.MainCommand => RaptureHotbarModule.HotbarSlotType.MainCommand,
|
||
HoverActionKind.ExtraCommand => RaptureHotbarModule.HotbarSlotType.ExtraCommand,
|
||
HoverActionKind.Companion => RaptureHotbarModule.HotbarSlotType.Companion,
|
||
HoverActionKind.PetOrder => RaptureHotbarModule.HotbarSlotType.PetAction,
|
||
HoverActionKind.BuddyAction => RaptureHotbarModule.HotbarSlotType.BuddyAction,
|
||
HoverActionKind.Mount => RaptureHotbarModule.HotbarSlotType.Mount,
|
||
HoverActionKind.BgcArmyAction => RaptureHotbarModule.HotbarSlotType.BgcArmyAction,
|
||
HoverActionKind.Perform => RaptureHotbarModule.HotbarSlotType.PerformanceInstrument,
|
||
HoverActionKind.Ornament => RaptureHotbarModule.HotbarSlotType.Ornament,
|
||
HoverActionKind.Glasses => RaptureHotbarModule.HotbarSlotType.Glasses,
|
||
HoverActionKind.MYCTemporaryItem => RaptureHotbarModule.HotbarSlotType.LostFindsItem,
|
||
HoverActionKind.QuickChat => RaptureHotbarModule.HotbarSlotType.PvPQuickChat,
|
||
HoverActionKind.ActionComboRoute => RaptureHotbarModule.HotbarSlotType.PvPCombo,
|
||
_ => RaptureHotbarModule.HotbarSlotType.Empty
|
||
};
|
||
}
|
||
|
||
private static unsafe uint GetIconIdForPayload(RaptureHotbarModule.HotbarSlotType slotType, uint id)
|
||
{
|
||
if (id == 0) return 0;
|
||
if (slotType == RaptureHotbarModule.HotbarSlotType.Macro)
|
||
{
|
||
var macroModule = RaptureMacroModule.Instance();
|
||
if (macroModule != null && id is >= 1 and <= 200)
|
||
{
|
||
uint set = (id - 1) / 100;
|
||
uint index1Based = ((id - 1) % 100) + 1; // GetMacro expects 1-based index
|
||
var macro = macroModule->GetMacro(set, index1Based);
|
||
if (macro != null)
|
||
return macro->IconId;
|
||
}
|
||
return 0;
|
||
}
|
||
var module = RaptureHotbarModule.Instance();
|
||
if (module != null && module->ModuleReady)
|
||
{
|
||
var bar = module->StandardHotbars[0];
|
||
var slot = bar.GetHotbarSlot(0);
|
||
if (slot != null)
|
||
{
|
||
int gameIcon = slot->GetIconIdForSlot(slotType, id);
|
||
if (gameIcon > 0) return (uint)gameIcon;
|
||
}
|
||
}
|
||
try
|
||
{
|
||
switch (slotType)
|
||
{
|
||
case RaptureHotbarModule.HotbarSlotType.GeneralAction:
|
||
var gaRow = Plugin.DataManager.GetExcelSheet<GeneralAction>()?.GetRow(id);
|
||
return gaRow.HasValue ? (uint)gaRow.Value.Icon : 0;
|
||
case RaptureHotbarModule.HotbarSlotType.Action:
|
||
case RaptureHotbarModule.HotbarSlotType.CraftAction:
|
||
case RaptureHotbarModule.HotbarSlotType.PetAction:
|
||
var action = Plugin.DataManager.GetExcelSheet<LuminaAction>()?.GetRow(id);
|
||
return action.HasValue ? (uint)action.Value.Icon : 0;
|
||
case RaptureHotbarModule.HotbarSlotType.Item:
|
||
uint baseItemId = id >= 1000000 ? id - 1000000 : id;
|
||
var item = Plugin.DataManager.GetExcelSheet<Item>()?.GetRow(baseItemId);
|
||
return item.HasValue ? (uint)item.Value.Icon : 0;
|
||
case RaptureHotbarModule.HotbarSlotType.Mount:
|
||
var mount = Plugin.DataManager.GetExcelSheet<Mount>()?.GetRow(id);
|
||
return mount.HasValue ? (uint)mount.Value.Icon : 0;
|
||
case RaptureHotbarModule.HotbarSlotType.Companion:
|
||
var companion = Plugin.DataManager.GetExcelSheet<Companion>()?.GetRow(id);
|
||
return companion.HasValue ? (uint)companion.Value.Icon : 0;
|
||
case RaptureHotbarModule.HotbarSlotType.Emote:
|
||
var emote = Plugin.DataManager.GetExcelSheet<Emote>()?.GetRow(id);
|
||
return emote.HasValue ? (uint)emote.Value.Icon : 0;
|
||
default:
|
||
return 0;
|
||
}
|
||
}
|
||
catch { return 0; }
|
||
}
|
||
|
||
private static unsafe (string title, string text) GetSlotTooltip(ActionBarsManager.SlotInfo slot)
|
||
{
|
||
// GeneralAction uses a different ID space: GeneralAction 7 = Teleport links to Action 5. Look up via GeneralAction sheet.
|
||
if (slot.SlotType == RaptureHotbarModule.HotbarSlotType.GeneralAction)
|
||
{
|
||
if (Plugin.DataManager.GetExcelSheet<GeneralAction>()?.TryGetRow(slot.ActionId, out var gaRow) == true)
|
||
{
|
||
string name = gaRow.Name.ToString();
|
||
string desc = "";
|
||
try
|
||
{
|
||
string descRaw = gaRow.Description.ToDalamudString().ToString();
|
||
if (!string.IsNullOrEmpty(descRaw))
|
||
{
|
||
try
|
||
{
|
||
var evaluated = Plugin.SeStringEvaluator.Evaluate(gaRow.Description.AsSpan());
|
||
desc = evaluated.ExtractText();
|
||
if (string.IsNullOrEmpty(desc)) desc = descRaw;
|
||
}
|
||
catch { desc = descRaw; }
|
||
if (!string.IsNullOrEmpty(desc))
|
||
desc = EncryptedStringsHelper.GetString(desc);
|
||
}
|
||
}
|
||
catch { /* ignore */ }
|
||
string statsLine = "";
|
||
if (gaRow.Action.RowId != 0)
|
||
{
|
||
var actionRow = Plugin.DataManager.GetExcelSheet<LuminaAction>()?.GetRow(gaRow.Action.RowId);
|
||
if (actionRow.HasValue)
|
||
statsLine = TryGetActionStatsLine(actionRow.Value, includeRangeAndRadius: false);
|
||
}
|
||
if (!string.IsNullOrEmpty(statsLine))
|
||
desc = string.IsNullOrEmpty(desc) ? statsLine : statsLine + "\n\n" + desc;
|
||
return (name, desc ?? "");
|
||
}
|
||
}
|
||
|
||
if (slot.SlotType == RaptureHotbarModule.HotbarSlotType.Action ||
|
||
slot.SlotType == RaptureHotbarModule.HotbarSlotType.CraftAction ||
|
||
slot.SlotType == RaptureHotbarModule.HotbarSlotType.PetAction)
|
||
{
|
||
var row = Plugin.DataManager.GetExcelSheet<LuminaAction>()?.GetRow(slot.ActionId);
|
||
if (row.HasValue)
|
||
{
|
||
string name = row.Value.Name.ToString();
|
||
string desc = "";
|
||
var descRow = Plugin.DataManager.GetExcelSheet<ActionTransient>()?.GetRow(slot.ActionId);
|
||
string descRaw = "";
|
||
if (descRow.HasValue)
|
||
{
|
||
try
|
||
{
|
||
var descSeStr = descRow.Value.Description;
|
||
descRaw = descRow.Value.Description.ToDalamudString().ToString();
|
||
try
|
||
{
|
||
var evaluated = Plugin.SeStringEvaluator.Evaluate(descSeStr.AsSpan());
|
||
desc = evaluated.ExtractText();
|
||
if (string.IsNullOrEmpty(desc)) desc = descRaw;
|
||
}
|
||
catch
|
||
{
|
||
desc = descRaw;
|
||
}
|
||
if (!string.IsNullOrEmpty(desc))
|
||
desc = EncryptedStringsHelper.GetString(desc);
|
||
}
|
||
catch { /* ignore */ }
|
||
}
|
||
bool isCombatAction = IsCombatAction(slot.SlotType, row.Value);
|
||
int? potencyValue = isCombatAction ? TryGetPotencyValue(row.Value) : null;
|
||
string potencyLine = potencyValue.HasValue ? $"Potency: {potencyValue.Value}" : "";
|
||
string statsLine = TryGetActionStatsLine(row.Value, includeRangeAndRadius: isCombatAction);
|
||
if (!string.IsNullOrEmpty(desc) && potencyValue.HasValue)
|
||
{
|
||
string before = desc;
|
||
desc = ReplacePotencyPlaceholderInDesc(desc, potencyValue.Value);
|
||
if (IsTooltipDebugEnabled() && before != desc)
|
||
Plugin.Logger.Information($"[HSUI Tooltip DBG] Replaced potency placeholder: actionId={slot.ActionId} value={potencyValue.Value}");
|
||
}
|
||
else if (IsTooltipDebugEnabled() && !string.IsNullOrEmpty(desc) && desc.Contains("potency of", StringComparison.OrdinalIgnoreCase))
|
||
Plugin.Logger.Information($"[HSUI Tooltip DBG] Potency placeholder NOT replaced: actionId={slot.ActionId} potencyValue={potencyValue?.ToString() ?? "null"} descSnippet='{(desc.Length > 60 ? desc[..60] + "..." : desc)}'");
|
||
var statsParts = new List<string>();
|
||
if (!string.IsNullOrEmpty(potencyLine)) statsParts.Add(potencyLine);
|
||
if (!string.IsNullOrEmpty(statsLine)) statsParts.Add(statsLine);
|
||
if (statsParts.Count > 0)
|
||
desc = string.IsNullOrEmpty(desc) ? string.Join("\n", statsParts) : string.Join("\n", statsParts) + "\n\n" + desc;
|
||
|
||
if (IsTooltipDebugEnabled() && ShouldLogTooltipDebug(slot.ActionId, name, desc))
|
||
Plugin.Logger.Information($"[HSUI Tooltip DBG] GetSlotTooltip actionId={slot.ActionId} name='{name}' descRawLen={descRaw?.Length ?? 0} descLen={desc?.Length ?? 0} descRawPreview='{(descRaw?.Length > 0 ? (descRaw.Length > 50 ? descRaw[..50] + "..." : descRaw) : "(empty)")}' fullBodyPreview='{(desc != null && desc.Length > 100 ? desc[..100] + "..." : desc ?? "")}'");
|
||
return (name, desc ?? "");
|
||
}
|
||
}
|
||
|
||
if (slot.SlotType == RaptureHotbarModule.HotbarSlotType.Macro)
|
||
{
|
||
try
|
||
{
|
||
var macroModule = RaptureMacroModule.Instance();
|
||
if (macroModule != null && slot.ActionId is >= 1 and <= 200)
|
||
{
|
||
uint set = (slot.ActionId - 1) / 100;
|
||
uint index1Based = ((slot.ActionId - 1) % 100) + 1; // GetMacro expects 1-based index
|
||
var macro = macroModule->GetMacro(set, index1Based);
|
||
if (macro != null)
|
||
{
|
||
string name = macro->Name.ToString();
|
||
if (string.IsNullOrWhiteSpace(name))
|
||
name = (set == 0 ? "Individual" : "Shared") + " Macro " + index1Based;
|
||
var sb = new System.Text.StringBuilder();
|
||
var lineCount = (int)macroModule->GetLineCount(macro);
|
||
for (int i = 0; i < lineCount && i < 15; i++)
|
||
{
|
||
ref var line = ref macro->Lines[i];
|
||
if (!line.IsEmpty)
|
||
{
|
||
if (sb.Length > 0) sb.Append('\n');
|
||
sb.Append(line.ToString());
|
||
}
|
||
}
|
||
string desc = sb.ToString();
|
||
return (name, desc);
|
||
}
|
||
}
|
||
}
|
||
catch { }
|
||
return ("Macro", "");
|
||
}
|
||
|
||
if (slot.SlotType == RaptureHotbarModule.HotbarSlotType.Item)
|
||
{
|
||
var row = Plugin.DataManager.GetExcelSheet<Item>()?.GetRow(slot.ActionId);
|
||
if (row.HasValue)
|
||
{
|
||
string name = row.Value.Name.ToString();
|
||
try
|
||
{
|
||
string desc = row.Value.Description.ToDalamudString().ToString();
|
||
if (!string.IsNullOrEmpty(desc))
|
||
desc = EncryptedStringsHelper.GetString(desc);
|
||
return (name, desc);
|
||
}
|
||
catch
|
||
{
|
||
return (name, "");
|
||
}
|
||
}
|
||
}
|
||
|
||
return (slot.SlotType.ToString(), "");
|
||
}
|
||
|
||
private static int? TryGetPotencyValue(LuminaAction action)
|
||
{
|
||
try
|
||
{
|
||
var t = typeof(LuminaAction);
|
||
foreach (var name in new[] { "AttackPotency", "Effect1", "Effect2", "Effect3", "PrimaryEffect", "SecondaryEffect" })
|
||
{
|
||
var prop = t.GetProperty(name, BindingFlags.Public | BindingFlags.Instance);
|
||
if (prop == null) continue;
|
||
var v = prop.GetValue(action);
|
||
if (v is int i && i > 0 && i <= 5000) return i;
|
||
if (v is uint u && u > 0 && u <= 5000) return (int)u;
|
||
}
|
||
// Fallback: try Omen row - potency may be stored there for weaponskills
|
||
try
|
||
{
|
||
var omenRef = action.Omen;
|
||
uint omenRowId = omenRef.RowId;
|
||
if (omenRowId != 0)
|
||
{
|
||
var omenSheet = Plugin.DataManager.GetExcelSheet<Omen>();
|
||
if (omenSheet != null && omenSheet.GetRow(omenRowId) is { } omenRow)
|
||
{
|
||
var ot = typeof(Omen);
|
||
foreach (var pname in new[] { "Effect1", "Effect2", "Effect3", "AttackPotency" })
|
||
{
|
||
var op = ot.GetProperty(pname, BindingFlags.Public | BindingFlags.Instance);
|
||
if (op == null) continue;
|
||
var ov = op.GetValue(omenRow);
|
||
if (ov is int oi && oi >= 50 && oi <= 2000) return oi;
|
||
if (ov is uint ou && ou >= 50 && ou <= 2000) return (int)ou;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch { /* ignore */ }
|
||
// Last resort: scan Action int/uint properties
|
||
foreach (var prop in t.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
||
{
|
||
if (prop.PropertyType != typeof(int) && prop.PropertyType != typeof(uint)) continue;
|
||
var name = prop.Name;
|
||
if (name.Contains("Id") || name.Contains("Index") || name.Contains("Time") ||
|
||
name.Contains("Range") || name.Contains("Radius") || name.Contains("Cost") ||
|
||
name.Contains("Level") || name.Contains("Category") || name.Contains("Cast") ||
|
||
name.Contains("Recast") || name.Contains("Omen"))
|
||
continue;
|
||
var v = prop.GetValue(action);
|
||
if (v is int i && i >= 50 && i <= 2000) return i;
|
||
if (v is uint u && u >= 50 && u <= 2000) return (int)u;
|
||
}
|
||
}
|
||
catch { /* ignore */ }
|
||
return null;
|
||
}
|
||
|
||
private static string ReplacePotencyPlaceholderInDesc(string desc, int potencyValue)
|
||
{
|
||
if (string.IsNullOrEmpty(desc)) return desc;
|
||
string replaced = desc;
|
||
// Game uses placeholder that can render as ".", "?", etc. Try common patterns
|
||
replaced = System.Text.RegularExpressions.Regex.Replace(replaced,
|
||
@"(potency of\s+)[^0-9]{1,4}",
|
||
$"$1{potencyValue}",
|
||
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
||
if (replaced == desc && desc.Contains("potency of .", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
int idx = desc.IndexOf("potency of .", StringComparison.OrdinalIgnoreCase);
|
||
if (idx >= 0)
|
||
replaced = desc[..(idx + 10)] + potencyValue + desc[(idx + 11)..];
|
||
}
|
||
return replaced;
|
||
}
|
||
|
||
/// <summary>Returns true if this action should show combat stats (potency, range, radius). False for Teleport, Return, Sprint, etc.</summary>
|
||
private static bool IsCombatAction(RaptureHotbarModule.HotbarSlotType slotType, LuminaAction action)
|
||
{
|
||
// GeneralAction: Teleport, Return, Sprint, Limit Break - never show combat stats
|
||
if (slotType == RaptureHotbarModule.HotbarSlotType.GeneralAction)
|
||
return false;
|
||
// CraftAction: crafting actions have cast/recast but no combat range/radius
|
||
if (slotType == RaptureHotbarModule.HotbarSlotType.CraftAction)
|
||
return false;
|
||
// PetAction: usually combat, but some pets have non-combat skills - keep combat stats for now
|
||
// Action: check ActionCategory - category 4 (General) and similar are non-combat
|
||
try
|
||
{
|
||
var catRef = action.ActionCategory;
|
||
uint rowId = catRef.RowId;
|
||
if (rowId is 4 or 5) return false; // General, Other
|
||
}
|
||
catch { /* ignore */ }
|
||
return true;
|
||
}
|
||
|
||
private static string TryGetActionStatsLine(LuminaAction action, bool includeRangeAndRadius = true)
|
||
{
|
||
try
|
||
{
|
||
var parts = new List<string>();
|
||
float castSec = action.Cast100ms / 100f;
|
||
float recastSec = action.Recast100ms / 100f;
|
||
if (castSec > 0)
|
||
parts.Add($"Cast: {castSec:F1}s");
|
||
else
|
||
parts.Add("Cast: Instant");
|
||
if (recastSec > 0)
|
||
parts.Add($"Recast: {recastSec:F1}s");
|
||
if (includeRangeAndRadius)
|
||
{
|
||
int range = action.Range;
|
||
int effectRange = action.EffectRange;
|
||
if (range > 0)
|
||
parts.Add($"Range: {range}y");
|
||
if (effectRange > 0)
|
||
parts.Add($"Radius: {effectRange}y");
|
||
}
|
||
return parts.Count > 0 ? string.Join(" | ", parts) : "";
|
||
}
|
||
catch { return ""; }
|
||
}
|
||
}
|
||
}
|