Initial release: HSUI v1.0.0.0 - HUD replacement with configurable hotbars
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
using HSUI.Config;
|
||||
using HSUI.Enums;
|
||||
using HSUI.Interface.GeneralElements;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace HSUI.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Hit test for HSUI hotbars. Used to determine if the cursor is over an HSUI hotbar
|
||||
/// so we only intercept drag-drop releases when the user is actually dropping on our bars.
|
||||
/// </summary>
|
||||
public static class ActionBarsHitTestHelper
|
||||
{
|
||||
/// <summary>Returns true if the mouse cursor is over any visible HSUI hotbar.</summary>
|
||||
public static bool IsMouseOverAnyHSUIHotbar()
|
||||
{
|
||||
try
|
||||
{
|
||||
var hotbarsConfig = ConfigurationManager.Instance?.GetConfigObject<HotbarsConfig>();
|
||||
if (hotbarsConfig == null || !hotbarsConfig.Enabled)
|
||||
return false;
|
||||
|
||||
var hudOptions = ConfigurationManager.Instance?.GetConfigObject<HUDOptionsConfig>();
|
||||
Vector2 origin = ImGui.GetMainViewport().Size / 2f;
|
||||
if (hudOptions != null && hudOptions.UseGlobalHudShift)
|
||||
origin += hudOptions.HudOffset;
|
||||
|
||||
Vector2 mousePos = ImGui.GetMousePos();
|
||||
|
||||
HotbarBarConfig?[] barConfigs = new HotbarBarConfig?[]
|
||||
{
|
||||
ConfigurationManager.Instance?.GetConfigObject<Hotbar1BarConfig>(),
|
||||
ConfigurationManager.Instance?.GetConfigObject<Hotbar2BarConfig>(),
|
||||
ConfigurationManager.Instance?.GetConfigObject<Hotbar3BarConfig>(),
|
||||
ConfigurationManager.Instance?.GetConfigObject<Hotbar4BarConfig>(),
|
||||
ConfigurationManager.Instance?.GetConfigObject<Hotbar5BarConfig>(),
|
||||
ConfigurationManager.Instance?.GetConfigObject<Hotbar6BarConfig>(),
|
||||
ConfigurationManager.Instance?.GetConfigObject<Hotbar7BarConfig>(),
|
||||
ConfigurationManager.Instance?.GetConfigObject<Hotbar8BarConfig>(),
|
||||
ConfigurationManager.Instance?.GetConfigObject<Hotbar9BarConfig>(),
|
||||
ConfigurationManager.Instance?.GetConfigObject<Hotbar10BarConfig>()
|
||||
};
|
||||
|
||||
foreach (var barConfig in barConfigs)
|
||||
{
|
||||
if (barConfig == null) continue;
|
||||
|
||||
Vector2 barSize = ComputeBarSize(barConfig);
|
||||
Vector2 topLeft = Utils.GetAnchoredPosition(origin + barConfig.Position, barSize, barConfig.Anchor);
|
||||
|
||||
if (mousePos.X >= topLeft.X && mousePos.X < topLeft.X + barSize.X &&
|
||||
mousePos.Y >= topLeft.Y && mousePos.Y < topLeft.Y + barSize.Y)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static Vector2 ComputeBarSize(HotbarBarConfig config)
|
||||
{
|
||||
var (cols, _) = 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,457 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.String;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
||||
using KamiToolKit.Controllers;
|
||||
|
||||
namespace HSUI.Helpers
|
||||
{
|
||||
public sealed class ActionBarsManager : IDisposable
|
||||
{
|
||||
public static ActionBarsManager Instance { get; private set; } = null!;
|
||||
|
||||
private AddonController? _addonController;
|
||||
|
||||
private ActionBarsManager()
|
||||
{
|
||||
_addonController = new AddonController("_ActionBar");
|
||||
_addonController.Enable();
|
||||
}
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
Instance = new ActionBarsManager();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_addonController?.Disable();
|
||||
_addonController = null;
|
||||
Instance = null!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Slot data for ImGui drawing. Updated each call from game state.
|
||||
/// </summary>
|
||||
public readonly struct SlotInfo
|
||||
{
|
||||
public uint IconId { get; }
|
||||
public bool IsEmpty { get; }
|
||||
public bool IsUsable { get; }
|
||||
public int CooldownPercent { get; }
|
||||
public int CooldownSecondsLeft { get; }
|
||||
public uint ActionId { get; }
|
||||
public RaptureHotbarModule.HotbarSlotType SlotType { get; }
|
||||
/// <summary>Keybind hint from game (user's keybind settings). Empty if unavailable.</summary>
|
||||
public string KeybindHint { get; }
|
||||
|
||||
public SlotInfo(uint iconId, bool isEmpty, bool usable, int cooldownPct, int cooldownSecs, uint actionId = 0, RaptureHotbarModule.HotbarSlotType slotType = 0, string keybindHint = "")
|
||||
{
|
||||
IconId = iconId;
|
||||
IsEmpty = isEmpty;
|
||||
IsUsable = usable;
|
||||
CooldownPercent = cooldownPct;
|
||||
CooldownSecondsLeft = cooldownSecs;
|
||||
ActionId = actionId;
|
||||
SlotType = slotType;
|
||||
KeybindHint = keybindHint ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads keybind hint from HotbarSlot. Uses _keybindHint (slot display) then _popUpKeybindHint (trimmed) as fallback.
|
||||
/// Offsets match RaptureHotbarModule.HotbarSlot: _keybindHint 0xA8, _popUpKeybindHint 0x88.
|
||||
/// </summary>
|
||||
private static unsafe string ReadKeybindHintFromSlot(RaptureHotbarModule.HotbarSlot* slot)
|
||||
{
|
||||
if (slot == null) return "";
|
||||
var sb = (byte*)slot;
|
||||
string? h = Marshal.PtrToStringUTF8((IntPtr)(sb + 0xA8));
|
||||
if (!string.IsNullOrWhiteSpace(h))
|
||||
return h.Trim();
|
||||
string? p = Marshal.PtrToStringUTF8((IntPtr)(sb + 0x88));
|
||||
if (string.IsNullOrWhiteSpace(p)) return "";
|
||||
p = p!.Trim();
|
||||
if (p.StartsWith(" [", StringComparison.Ordinal) && p.EndsWith("]", StringComparison.Ordinal))
|
||||
p = p[2..^1].Trim();
|
||||
return p;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads hotbar slot data from RaptureHotbarModule. Returns up to slotCount slots.
|
||||
/// hotbarIndex 1-10 maps to StandardHotbars 0-9.
|
||||
/// </summary>
|
||||
public unsafe List<SlotInfo> GetSlotData(int hotbarIndex, int slotCount)
|
||||
{
|
||||
var list = new List<SlotInfo>(slotCount);
|
||||
var module = RaptureHotbarModule.Instance();
|
||||
if (module == null || !module->ModuleReady)
|
||||
return list;
|
||||
|
||||
var hotbars = module->StandardHotbars;
|
||||
int barIdx = Math.Clamp(hotbarIndex, 1, 10) - 1;
|
||||
int count = Math.Clamp(slotCount, 1, 12);
|
||||
|
||||
ref var bar = ref hotbars[barIdx];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var slot = bar.GetHotbarSlot((uint)i);
|
||||
string keybind = ReadKeybindHintFromSlot(slot);
|
||||
|
||||
if (slot == null)
|
||||
{
|
||||
list.Add(new SlotInfo(0, true, false, 0, 0, 0, 0, keybind));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (slot->IsEmpty)
|
||||
{
|
||||
list.Add(new SlotInfo(0, true, false, 0, 0, 0, 0, keybind));
|
||||
continue;
|
||||
}
|
||||
|
||||
int secsLeft = 0;
|
||||
int pct = slot->GetSlotActionCooldownPercentage(&secsLeft, 0);
|
||||
bool usable = slot->IsSlotUsable(slot->ApparentSlotType, slot->ApparentActionId);
|
||||
uint iconId = slot->IconId;
|
||||
uint actionId = slot->ApparentActionId;
|
||||
var slotType = slot->ApparentSlotType;
|
||||
|
||||
list.Add(new SlotInfo(iconId, false, usable, pct, secsLeft, actionId, slotType, keybind));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the default game keybind label for a hotbar slot (Hotbar 1: 1,2,...,0,-,=; Bar 2: Ctrl+1..12; etc.).
|
||||
/// hotbarIndex 1–10, slotIndex 0–11. Used to mirror the default hotbar keybind display.
|
||||
/// </summary>
|
||||
public static string GetDefaultKeybindLabel(int hotbarIndex, int slotIndex)
|
||||
{
|
||||
int s = Math.Clamp(slotIndex, 0, 11);
|
||||
string k = s switch
|
||||
{
|
||||
0 => "1", 1 => "2", 2 => "3", 3 => "4", 4 => "5", 5 => "6",
|
||||
6 => "7", 7 => "8", 8 => "9", 9 => "0", 10 => "-", 11 => "=",
|
||||
_ => (s + 1).ToString()
|
||||
};
|
||||
int b = Math.Clamp(hotbarIndex, 1, 10);
|
||||
return b switch
|
||||
{
|
||||
1 => k,
|
||||
2 => "Ctrl+" + k,
|
||||
3 => "Shift+" + k,
|
||||
4 => "Alt+" + k,
|
||||
_ => $"{b}-{s + 1}"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute a hotbar slot. hotbarIndex 1-10, slotIndex 0-based.
|
||||
/// </summary>
|
||||
public unsafe bool ExecuteSlot(int hotbarIndex, int slotIndex)
|
||||
{
|
||||
var module = RaptureHotbarModule.Instance();
|
||||
if (module == null || !module->ModuleReady)
|
||||
return false;
|
||||
|
||||
int barIdx = Math.Clamp(hotbarIndex, 1, 10) - 1;
|
||||
int slot = Math.Clamp(slotIndex, 0, 11);
|
||||
module->ExecuteSlotById((uint)barIdx, (uint)slot);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Swap two hotbar slots. Supports cross-hotbar swap when hotbarA != hotbarB.
|
||||
/// Uses CommandType/CommandId from the slot (not Apparent*) so items and macros swap correctly.
|
||||
/// hotbarIndex 1-10, slot indices 0-based.
|
||||
/// </summary>
|
||||
/// <param name="debugLog">When non-null, receives diagnostic info for logging.</param>
|
||||
public unsafe bool SwapSlots(int hotbarA, int slotA, int hotbarB, int slotB, Action<string>? debugLog = null)
|
||||
{
|
||||
var module = RaptureHotbarModule.Instance();
|
||||
if (module == null || !module->ModuleReady)
|
||||
{
|
||||
debugLog?.Invoke($"[SwapSlots] FAIL: module null or not ready");
|
||||
return false;
|
||||
}
|
||||
|
||||
int barA = Math.Clamp(hotbarA, 1, 10);
|
||||
int barB = Math.Clamp(hotbarB, 1, 10);
|
||||
int a = Math.Clamp(slotA, 0, 11);
|
||||
int b = Math.Clamp(slotB, 0, 11);
|
||||
if (barA == barB && a == b)
|
||||
{
|
||||
debugLog?.Invoke($"[SwapSlots] NOOP: same slot bar={barA} slot={a}");
|
||||
return true;
|
||||
}
|
||||
|
||||
var slotPtrA = module->GetSlotById((uint)(barA - 1), (uint)a);
|
||||
var slotPtrB = module->GetSlotById((uint)(barB - 1), (uint)b);
|
||||
if (slotPtrA == null || slotPtrB == null)
|
||||
{
|
||||
debugLog?.Invoke($"[SwapSlots] FAIL: slotPtr null A={slotPtrA != null} B={slotPtrB != null}");
|
||||
return false;
|
||||
}
|
||||
|
||||
var typeA = slotPtrA->IsEmpty ? RaptureHotbarModule.HotbarSlotType.Empty : slotPtrA->CommandType;
|
||||
var idA = slotPtrA->IsEmpty ? 0u : slotPtrA->CommandId;
|
||||
var typeB = slotPtrB->IsEmpty ? RaptureHotbarModule.HotbarSlotType.Empty : slotPtrB->CommandType;
|
||||
var idB = slotPtrB->IsEmpty ? 0u : slotPtrB->CommandId;
|
||||
|
||||
debugLog?.Invoke($"[SwapSlots] BEFORE: A(bar{barA} slot{a}) type={typeA} id={idA} | B(bar{barB} slot{b}) type={typeB} id={idB}");
|
||||
|
||||
// Update live slots first (like game drag-drop), then persist
|
||||
slotPtrA->Set(typeB, idB);
|
||||
slotPtrA->LoadIconId();
|
||||
if (typeB == RaptureHotbarModule.HotbarSlotType.Item)
|
||||
slotPtrA->LoadCostDataForSlot(true);
|
||||
slotPtrB->Set(typeA, idA);
|
||||
slotPtrB->LoadIconId();
|
||||
if (typeA == RaptureHotbarModule.HotbarSlotType.Item)
|
||||
slotPtrB->LoadCostDataForSlot(true);
|
||||
module->SetAndSaveSlot((uint)(barA - 1), (uint)a, typeB, idB);
|
||||
module->SetAndSaveSlot((uint)(barB - 1), (uint)b, typeA, idA);
|
||||
|
||||
// Read back to verify
|
||||
slotPtrA = module->GetSlotById((uint)(barA - 1), (uint)a);
|
||||
slotPtrB = module->GetSlotById((uint)(barB - 1), (uint)b);
|
||||
if (slotPtrA != null && slotPtrB != null)
|
||||
{
|
||||
var afterA = slotPtrA->IsEmpty ? "empty" : $"{slotPtrA->CommandType} id={slotPtrA->CommandId}";
|
||||
var afterB = slotPtrB->IsEmpty ? "empty" : $"{slotPtrB->CommandType} id={slotPtrB->CommandId}";
|
||||
debugLog?.Invoke($"[SwapSlots] AFTER: A(bar{barA} slot{a}) {afterA} | B(bar{barB} slot{b}) {afterB}");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Dump HotbarSlot and RaptureMacroModule.Macro memory for a slot (for macro persistence debugging).</summary>
|
||||
/// <param name="hotbarIndex">1-10</param>
|
||||
/// <param name="slotIndex">0-11</param>
|
||||
public unsafe void DumpMacroSlotMemoryToLog(int hotbarIndex, int slotIndex)
|
||||
{
|
||||
var hotbarModule = RaptureHotbarModule.Instance();
|
||||
if (hotbarModule == null || !hotbarModule->ModuleReady)
|
||||
{
|
||||
Plugin.Logger.Information("[HSUI Macro DBG] RaptureHotbarModule not ready");
|
||||
return;
|
||||
}
|
||||
int barIdx = Math.Clamp(hotbarIndex, 1, 10) - 1;
|
||||
int slot = Math.Clamp(slotIndex, 0, 11);
|
||||
var slotPtr = hotbarModule->GetSlotById((uint)barIdx, (uint)slot);
|
||||
if (slotPtr == null)
|
||||
{
|
||||
Plugin.Logger.Information($"[HSUI Macro DBG] Bar {hotbarIndex} slot {slotIndex}: slot is null");
|
||||
return;
|
||||
}
|
||||
var ct = slotPtr->CommandType;
|
||||
var cid = slotPtr->CommandId;
|
||||
Plugin.Logger.Information($"[HSUI Macro DBG] Bar {hotbarIndex} slot {slotIndex}: CommandType={ct} CommandId={cid} ApparentActionId={slotPtr->ApparentActionId} ApparentSlotType={slotPtr->ApparentSlotType} IconId={slotPtr->IconId}");
|
||||
if (ct == RaptureHotbarModule.HotbarSlotType.Macro && cid is >= 1 and <= 200)
|
||||
{
|
||||
byte macroSet = (byte)((cid - 1) / 100);
|
||||
byte macroIdx0Based = (byte)((cid - 1) % 100);
|
||||
uint macroIdx1Based = (uint)(macroIdx0Based + 1); // GetMacro expects 1-based index
|
||||
var macroModule = RaptureMacroModule.Instance();
|
||||
if (macroModule != null)
|
||||
{
|
||||
var macro = macroModule->GetMacro(macroSet, macroIdx1Based);
|
||||
if (macro != null)
|
||||
{
|
||||
string name = macro->Name.ToString() ?? "(null)";
|
||||
Plugin.Logger.Information($"[HSUI Macro DBG] RaptureMacroModule.Macro set={macroSet} idx1Based={macroIdx1Based}: IconId={macro->IconId} MacroIconRowId={macro->MacroIconRowId} Name='{name}'");
|
||||
|
||||
// Raw byte dump (first 0x80 bytes: IconId, MacroIconRowId, start of Name/Utf8String)
|
||||
var sb = new StringBuilder();
|
||||
var ptr = (byte*)macro;
|
||||
for (int i = 0; i < 0x80 && i < 0x688; i += 16)
|
||||
{
|
||||
sb.Clear();
|
||||
sb.Append($"[HSUI Macro DBG] Macro+0x{i:X3}:");
|
||||
for (int j = 0; j < 16 && i + j < 0x80; j++)
|
||||
sb.Append($" {(ptr[i + j]):X2}");
|
||||
Plugin.Logger.Information(sb.ToString());
|
||||
}
|
||||
|
||||
// Try AddonMacro _macroName (offset 0x798, Utf8String=0x68 each, 100 entries)
|
||||
try
|
||||
{
|
||||
var addonAddr = Plugin.GameGui?.GetAddonByName("Macro", 1).Address ?? IntPtr.Zero;
|
||||
if (addonAddr != IntPtr.Zero)
|
||||
{
|
||||
var addon = (AddonMacro*)addonAddr;
|
||||
int utf8Size = 0x68;
|
||||
int nameBase = 0x798;
|
||||
var namePtr = (Utf8String*)((byte*)addon + nameBase + macroIdx0Based * utf8Size);
|
||||
string addonName = namePtr->ToString() ?? "(null)";
|
||||
Plugin.Logger.Information($"[HSUI Macro DBG] AddonMacro._macroName[{macroIdx0Based}]: '{addonName}'");
|
||||
}
|
||||
else
|
||||
Plugin.Logger.Information("[HSUI Macro DBG] AddonMacro not found or not open");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Logger.Information($"[HSUI Macro DBG] AddonMacro read failed: {ex.Message}");
|
||||
}
|
||||
|
||||
// Try AgentMacro SelectedMacroSet/Index (only relevant when that macro is selected)
|
||||
try
|
||||
{
|
||||
var agentModule = FFXIVClientStructs.FFXIV.Client.UI.Agent.AgentModule.Instance();
|
||||
if (agentModule != null)
|
||||
{
|
||||
var macroAgent = agentModule->GetAgentByInternalId(FFXIVClientStructs.FFXIV.Client.UI.Agent.AgentId.Macro);
|
||||
if (macroAgent != null)
|
||||
{
|
||||
var agentMacro = (FFXIVClientStructs.FFXIV.Client.UI.Agent.AgentMacro*)macroAgent;
|
||||
Plugin.Logger.Information($"[HSUI Macro DBG] AgentMacro SelectedMacroSet={agentMacro->SelectedMacroSet} SelectedMacroIndex={agentMacro->SelectedMacroIndex}");
|
||||
if (agentMacro->SelectedMacroSet == macroSet && agentMacro->SelectedMacroIndex == macroIdx1Based)
|
||||
{
|
||||
string rawStr = agentMacro->RawMacroString.ToString() ?? "(null)";
|
||||
Plugin.Logger.Information($"[HSUI Macro DBG] AgentMacro (matches): RawMacroString(len={rawStr.Length})='{(rawStr.Length > 60 ? rawStr[..60] + "..." : rawStr)}'");
|
||||
}
|
||||
}
|
||||
else
|
||||
Plugin.Logger.Information("[HSUI Macro DBG] AgentMacro agent is null");
|
||||
}
|
||||
else
|
||||
Plugin.Logger.Information("[HSUI Macro DBG] AgentModule.Instance() is null");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Logger.Information($"[HSUI Macro DBG] AgentMacro read failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
else
|
||||
Plugin.Logger.Information($"[HSUI Macro DBG] RaptureMacroModule.GetMacro({macroSet},{macroIdx1Based}) returned null");
|
||||
}
|
||||
else
|
||||
Plugin.Logger.Information("[HSUI Macro DBG] RaptureMacroModule.Instance() is null");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Dump AddonMacro and AgentMacro state (open Macro menu first for best data).</summary>
|
||||
public unsafe void DumpMacroMenuToLog()
|
||||
{
|
||||
Plugin.Logger.Information("[HSUI MacroMenu DBG] === Macro menu debug ===");
|
||||
|
||||
// AddonMacro
|
||||
try
|
||||
{
|
||||
var addonAddr = Plugin.GameGui?.GetAddonByName("Macro", 1).Address ?? IntPtr.Zero;
|
||||
if (addonAddr != IntPtr.Zero)
|
||||
{
|
||||
var addon = (AddonMacro*)addonAddr;
|
||||
Plugin.Logger.Information($"[HSUI MacroMenu DBG] AddonMacro: SelectedPage={addon->SelectedPage} SelectedMacroIndex={addon->SelectedMacroIndex} DefaultIcon={addon->DefaultIcon}");
|
||||
const int utf8Size = 0x68;
|
||||
const int nameBase = 0x798;
|
||||
const int iconBase = 0x604;
|
||||
const int createdBase = 0x3038;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var namePtr = (Utf8String*)((byte*)addon + nameBase + i * utf8Size);
|
||||
int icon = *(int*)((byte*)addon + iconBase + i * 4);
|
||||
bool created = *((byte*)addon + createdBase + i) != 0;
|
||||
string name = namePtr->ToString() ?? "(empty)";
|
||||
Plugin.Logger.Information($"[HSUI MacroMenu DBG] AddonMacro[{i}]: Name='{name}' Icon={icon} Created={created}");
|
||||
}
|
||||
Plugin.Logger.Information("[HSUI MacroMenu DBG] ... (showing first 10 of 100)");
|
||||
}
|
||||
else
|
||||
Plugin.Logger.Information("[HSUI MacroMenu DBG] AddonMacro not found - open Macro menu (Character Config > Hotbars > Macros) first");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Logger.Information($"[HSUI MacroMenu DBG] AddonMacro failed: {ex.Message}");
|
||||
}
|
||||
|
||||
// AgentMacro
|
||||
try
|
||||
{
|
||||
var agentModule = FFXIVClientStructs.FFXIV.Client.UI.Agent.AgentModule.Instance();
|
||||
if (agentModule != null)
|
||||
{
|
||||
var macroAgent = agentModule->GetAgentByInternalId(FFXIVClientStructs.FFXIV.Client.UI.Agent.AgentId.Macro);
|
||||
if (macroAgent != null)
|
||||
{
|
||||
var agent = (FFXIVClientStructs.FFXIV.Client.UI.Agent.AgentMacro*)macroAgent;
|
||||
Plugin.Logger.Information($"[HSUI MacroMenu DBG] AgentMacro: SelectedMacroSet={agent->SelectedMacroSet} (0=Individual,1=Shared) SelectedMacroIndex={agent->SelectedMacroIndex}");
|
||||
string raw = agent->RawMacroString.ToString() ?? "(null)";
|
||||
string parsed = agent->ParsedMacroString.ToString() ?? "(null)";
|
||||
Plugin.Logger.Information($"[HSUI MacroMenu DBG] RawMacroString(len={raw.Length}): '{(raw.Length > 80 ? raw[..80] + "..." : raw)}'");
|
||||
Plugin.Logger.Information($"[HSUI MacroMenu DBG] ParsedMacroString(len={parsed.Length}): '{(parsed.Length > 80 ? parsed[..80] + "..." : parsed)}'");
|
||||
Plugin.Logger.Information($"[HSUI MacroMenu DBG] MacroIconCount={agent->MacroIconCount}");
|
||||
|
||||
var clip = &agent->ClipboardMacro;
|
||||
string clipName = clip->Name.ToString() ?? "(null)";
|
||||
Plugin.Logger.Information($"[HSUI MacroMenu DBG] ClipboardMacro: IconId={clip->IconId} MacroIconRowId={clip->MacroIconRowId} Name='{clipName}'");
|
||||
|
||||
var rm = RaptureMacroModule.Instance();
|
||||
if (rm != null)
|
||||
{
|
||||
var selectedMacro = rm->GetMacro(agent->SelectedMacroSet, agent->SelectedMacroIndex);
|
||||
if (selectedMacro != null)
|
||||
{
|
||||
string rName = selectedMacro->Name.ToString() ?? "(null)";
|
||||
Plugin.Logger.Information($"[HSUI MacroMenu DBG] RaptureMacroModule.GetMacro({agent->SelectedMacroSet},{agent->SelectedMacroIndex}): IconId={selectedMacro->IconId} MacroIconRowId={selectedMacro->MacroIconRowId} Name='{rName}'");
|
||||
}
|
||||
else
|
||||
Plugin.Logger.Information($"[HSUI MacroMenu DBG] RaptureMacroModule.GetMacro returned null");
|
||||
}
|
||||
}
|
||||
else
|
||||
Plugin.Logger.Information("[HSUI MacroMenu DBG] AgentMacro agent is null");
|
||||
}
|
||||
else
|
||||
Plugin.Logger.Information("[HSUI MacroMenu DBG] AgentModule.Instance() is null");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Logger.Information($"[HSUI MacroMenu DBG] AgentMacro failed: {ex.Message}");
|
||||
}
|
||||
|
||||
Plugin.Logger.Information("[HSUI MacroMenu DBG] === end ===");
|
||||
}
|
||||
|
||||
/// <summary>Dump all hotbar slot CommandType/CommandId to the log for SwapSlots diagnosis.</summary>
|
||||
public unsafe void DumpSlotStateToLog()
|
||||
{
|
||||
var module = RaptureHotbarModule.Instance();
|
||||
if (module == null || !module->ModuleReady) { Plugin.Logger.Information("[HSUI HotbarSlots] Module not ready"); return; }
|
||||
for (int bar = 1; bar <= 10; bar++)
|
||||
{
|
||||
var sb = new System.Text.StringBuilder();
|
||||
for (int s = 0; s < 12; s++)
|
||||
{
|
||||
var slot = module->GetSlotById((uint)(bar - 1), (uint)s);
|
||||
if (slot == null) { sb.Append("? "); continue; }
|
||||
if (slot->IsEmpty) { sb.Append("-- "); continue; }
|
||||
sb.Append($"{slot->CommandType}:{slot->CommandId} ");
|
||||
}
|
||||
Plugin.Logger.Information($"[HSUI HotbarSlots] Bar {bar}: {sb}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Clear a hotbar slot. hotbarIndex 1-10, slotIndex 0-based.</summary>
|
||||
public unsafe bool ClearSlot(int hotbarIndex, int slotIndex)
|
||||
{
|
||||
var module = RaptureHotbarModule.Instance();
|
||||
if (module == null || !module->ModuleReady) return false;
|
||||
int barIdx = Math.Clamp(hotbarIndex, 1, 10) - 1;
|
||||
int slot = Math.Clamp(slotIndex, 0, 11);
|
||||
var slotPtr = module->GetSlotById((uint)barIdx, (uint)slot);
|
||||
if (slotPtr != null)
|
||||
{
|
||||
slotPtr->Set(RaptureHotbarModule.HotbarSlotType.Empty, 0);
|
||||
slotPtr->LoadIconId();
|
||||
}
|
||||
module->SetAndSaveSlot((uint)barIdx, (uint)slot, RaptureHotbarModule.HotbarSlotType.Empty, 0);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Interface.Textures;
|
||||
using Dalamud.Interface.Textures.TextureWraps;
|
||||
using Dalamud.Logging;
|
||||
using HSUI.Config;
|
||||
using HSUI.Interface.GeneralElements;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
|
||||
namespace HSUI.Helpers
|
||||
{
|
||||
public struct BarTextureData
|
||||
{
|
||||
public string Name;
|
||||
public string Path;
|
||||
public bool IsCustom;
|
||||
|
||||
public BarTextureData(string name, string path, bool isCustom)
|
||||
{
|
||||
Name = name;
|
||||
Path = path;
|
||||
IsCustom = isCustom;
|
||||
}
|
||||
}
|
||||
|
||||
public class BarTexturesManager : IDisposable
|
||||
{
|
||||
#region Singleton
|
||||
private BarTexturesManager(string basePath)
|
||||
{
|
||||
DefaultBarTexturesPath = Path.GetDirectoryName(basePath) + "\\Media\\Images\\textures\\";
|
||||
}
|
||||
|
||||
public static void Initialize(string basePath)
|
||||
{
|
||||
Instance = new BarTexturesManager(basePath);
|
||||
}
|
||||
|
||||
public static BarTexturesManager Instance { get; private set; } = null!;
|
||||
private BarTexturesConfig? _config;
|
||||
|
||||
public void LoadConfig()
|
||||
{
|
||||
if (_config != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_config = ConfigurationManager.Instance.GetConfigObject<BarTexturesConfig>();
|
||||
ConfigurationManager.Instance.ResetEvent += OnConfigReset;
|
||||
|
||||
ReloadTextures();
|
||||
}
|
||||
|
||||
private void OnConfigReset(ConfigurationManager sender)
|
||||
{
|
||||
_config = sender.GetConfigObject<BarTexturesConfig>();
|
||||
}
|
||||
|
||||
~BarTexturesManager()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ConfigurationManager.Instance.ResetEvent -= OnConfigReset;
|
||||
Instance = null!;
|
||||
}
|
||||
#endregion
|
||||
|
||||
public readonly string DefaultBarTexturesPath;
|
||||
public static readonly string DefaultBarTextureName = "Default";
|
||||
|
||||
public bool DefaultFontBuilt { get; private set; }
|
||||
public ImFontPtr DefaultFont { get; private set; } = null;
|
||||
|
||||
private List<BarTextureData> _textures = new List<BarTextureData>();
|
||||
public IReadOnlyCollection<BarTextureData> BarTextures => _textures.AsReadOnly();
|
||||
|
||||
private List<string> _textureNames = new List<string>();
|
||||
public IReadOnlyCollection<string> BarTextureNames => _textureNames.AsReadOnly();
|
||||
|
||||
private Dictionary<string, ISharedImmediateTexture> _cache = new();
|
||||
|
||||
public IDalamudTextureWrap? GetBarTexture(string? name)
|
||||
{
|
||||
if (name == null || name == DefaultBarTextureName) { return null; }
|
||||
|
||||
// get cached texture
|
||||
if (_cache.TryGetValue(name, out ISharedImmediateTexture? cachedTexture) && cachedTexture != null)
|
||||
{
|
||||
return cachedTexture.GetWrapOrDefault();
|
||||
}
|
||||
|
||||
// lazy load
|
||||
BarTextureData? data = _textures.FirstOrDefault(o => o.Name == name);
|
||||
if (!data.HasValue) { return null; }
|
||||
|
||||
if (File.Exists(data.Value.Path))
|
||||
{
|
||||
try
|
||||
{
|
||||
ISharedImmediateTexture? texture = Plugin.TextureProvider.GetFromFile(data.Value.Path);
|
||||
if (texture != null)
|
||||
{
|
||||
_cache.Add(name, texture);
|
||||
}
|
||||
|
||||
return texture?.GetWrapOrDefault();
|
||||
}
|
||||
catch
|
||||
(Exception ex)
|
||||
{
|
||||
Plugin.Logger.Warning($"Image failed to load. {data.Value.Path}: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void ReloadTextures()
|
||||
{
|
||||
_textures.Clear();
|
||||
|
||||
// embedded textures
|
||||
_textures.AddRange(TexturesFromPath(DefaultBarTexturesPath, true));
|
||||
|
||||
// custom textures
|
||||
if (_config != null)
|
||||
{
|
||||
_textures.AddRange(TexturesFromPath(_config.ValidatedBarTexturesPath, true));
|
||||
}
|
||||
|
||||
// sort by name
|
||||
_textures = _textures.OrderBy(o => o.Name).ToList();
|
||||
|
||||
// default always first
|
||||
_textures.Insert(0, new BarTextureData(DefaultBarTextureName, "", false));
|
||||
|
||||
_textureNames = _textures.Select(o => o.Name).ToList();
|
||||
}
|
||||
|
||||
private List<BarTextureData> TexturesFromPath(string path, bool isCustom)
|
||||
{
|
||||
string[] textures;
|
||||
try
|
||||
{
|
||||
string[] allowedExtensions = new string[] { ".png", ".tga" };
|
||||
textures = Directory
|
||||
.GetFiles(path)
|
||||
.Where(file => allowedExtensions.Any(file.ToLower().EndsWith))
|
||||
.ToArray();
|
||||
}
|
||||
catch
|
||||
{
|
||||
textures = new string[0];
|
||||
}
|
||||
|
||||
List<BarTextureData> result = new List<BarTextureData>(textures.Length);
|
||||
|
||||
for (int i = 0; i < textures.Length; i++)
|
||||
{
|
||||
string name = SanitizedTextureName(textures[i].Replace(path, ""));
|
||||
result.Add(new BarTextureData(name, textures[i], isCustom));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private string SanitizedTextureName(string name)
|
||||
{
|
||||
return name
|
||||
.Replace(".png", "")
|
||||
.Replace(".PNG", "")
|
||||
.Replace(".tga", "")
|
||||
.Replace(".TGA", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
using HSUI.Config;
|
||||
using HSUI.Interface.GeneralElements;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace HSUI.Helpers
|
||||
{
|
||||
public class ClipRectsHelper
|
||||
{
|
||||
#region Singleton
|
||||
private ClipRectsHelper()
|
||||
{
|
||||
ConfigurationManager.Instance.ResetEvent += OnConfigReset;
|
||||
OnConfigReset(ConfigurationManager.Instance);
|
||||
|
||||
// other plugins can add clip rects for HSUI
|
||||
// rect start point = vector.X, vector.Y
|
||||
// rect end point = vector.Z, vector.W
|
||||
_thirdPartyClipRects = Plugin.PluginInterface.GetOrCreateData<Dictionary<string, Vector4>>(_sharedDataId, () => new());
|
||||
}
|
||||
|
||||
public static void Initialize() { Instance = new ClipRectsHelper(); }
|
||||
|
||||
public static ClipRectsHelper Instance { get; private set; } = null!;
|
||||
|
||||
~ClipRectsHelper()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ConfigurationManager.Instance.ResetEvent -= OnConfigReset;
|
||||
|
||||
Plugin.PluginInterface.RelinquishData(_sharedDataId);
|
||||
|
||||
Instance = null!;
|
||||
}
|
||||
#endregion
|
||||
|
||||
private WindowClippingConfig _config = null!;
|
||||
|
||||
private void OnConfigReset(ConfigurationManager sender)
|
||||
{
|
||||
_config = sender.GetConfigObject<WindowClippingConfig>();
|
||||
}
|
||||
|
||||
public bool Enabled => _config.Enabled;
|
||||
public WindowClippingMode? Mode => _config.Enabled ? _config.Mode : null;
|
||||
|
||||
private List<ClipRect> _clipRects = new List<ClipRect>();
|
||||
private List<ClipRect> _extraClipRects = new List<ClipRect>();
|
||||
|
||||
private static Dictionary<string, Vector4> _thirdPartyClipRects = new();
|
||||
private static string _sharedDataId = "HSUI.ClipRects";
|
||||
|
||||
private static List<string> _ignoredAddonNames = new List<string>()
|
||||
{
|
||||
"_FocusTargetInfo",
|
||||
};
|
||||
|
||||
private readonly string[] _hotbarAddonNames = { "_ActionBar", "_ActionBar01", "_ActionBar02", "_ActionBar03", "_ActionBar04", "_ActionBar05", "_ActionBar06", "_ActionBar07", "_ActionBar08", "_ActionBar09" };
|
||||
|
||||
public unsafe void Update()
|
||||
{
|
||||
if (!_config.Enabled) { return; }
|
||||
|
||||
_clipRects.Clear();
|
||||
_extraClipRects.Clear();
|
||||
|
||||
// find clip rects for game windows
|
||||
AtkStage* stage = AtkStage.Instance();
|
||||
if (stage == null) { return; }
|
||||
|
||||
RaptureAtkUnitManager* manager = stage->RaptureAtkUnitManager;
|
||||
if (manager == null) { return; }
|
||||
|
||||
AtkUnitList* loadedUnitsList = &manager->AtkUnitManager.AllLoadedUnitsList;
|
||||
if (loadedUnitsList == null) { return; }
|
||||
|
||||
for (int i = 0; i < loadedUnitsList->Count; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
AtkUnitBase* addon = *(AtkUnitBase**)Unsafe.AsPointer(ref loadedUnitsList->Entries[i]);
|
||||
if (addon == null || addon->RootNode == null || !addon->IsVisible || addon->WindowNode == null || addon->Scale == 0 || !addon->WindowNode->IsVisible())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string name = addon->NameString;
|
||||
if (_ignoredAddonNames.Contains(name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float margin = 5 * addon->Scale;
|
||||
float bottomMargin = 13 * addon->Scale;
|
||||
|
||||
Vector2 pos = new Vector2(addon->RootNode->X + margin, addon->RootNode->Y + margin);
|
||||
Vector2 size = new Vector2(
|
||||
addon->RootNode->Width * addon->Scale - margin,
|
||||
addon->RootNode->Height * addon->Scale - bottomMargin
|
||||
);
|
||||
|
||||
// just in case this causes weird issues / crashes (doubt it though...)
|
||||
ClipRect clipRect = new ClipRect(pos, pos + size);
|
||||
if (clipRect.Max.X < clipRect.Min.X || clipRect.Max.Y < clipRect.Min.Y)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_clipRects.Add(clipRect);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
if (_config.ThirdPartyClipRectsEnabled)
|
||||
{
|
||||
// find clip rects from other plugins
|
||||
Dictionary<string, Vector4> dict = _thirdPartyClipRects;
|
||||
foreach (Vector4 vector in dict.Values)
|
||||
{
|
||||
ClipRect clipRect = new ClipRect(new(vector.X, vector.Y), new(vector.Z, vector.W));
|
||||
_clipRects.Add(clipRect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<ClipRect> ActiveClipRects()
|
||||
{
|
||||
return [.. _clipRects, .. _extraClipRects];
|
||||
}
|
||||
|
||||
public void AddNameplatesClipRects()
|
||||
{
|
||||
if (!_config.NameplatesClipRectsEnabled) { return; }
|
||||
|
||||
// target cast bar
|
||||
ClipRect? targetCastbarClipRect = GetTargetCastbarClipRect();
|
||||
if (targetCastbarClipRect.HasValue)
|
||||
{
|
||||
_extraClipRects.Add(targetCastbarClipRect.Value);
|
||||
}
|
||||
|
||||
// hotbars
|
||||
_extraClipRects.AddRange(GetHotbarsClipRects());
|
||||
|
||||
// chat bubbles
|
||||
_extraClipRects.AddRange(GetNPCChatBubbleClipRect());
|
||||
_extraClipRects.AddRange(GetPlayerChatBubbleClipRect());
|
||||
}
|
||||
|
||||
public void RemoveNameplatesClipRects()
|
||||
{
|
||||
_extraClipRects.Clear();
|
||||
}
|
||||
|
||||
private unsafe ClipRect? GetTargetCastbarClipRect()
|
||||
{
|
||||
if (!_config.TargetCastbarClipRectEnabled) { return null; }
|
||||
|
||||
AtkUnitBase* addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("_TargetInfoCastBar", 1).Address;
|
||||
if (addon == null || !addon->IsVisible) { return null; }
|
||||
|
||||
AtkResNode* baseNode = addon->GetNodeById(2);
|
||||
AtkImageNode* imageNode = addon->GetImageNodeById(7);
|
||||
|
||||
if (baseNode == null || !baseNode->IsVisible()) { return null; }
|
||||
if (imageNode == null || !imageNode->IsVisible()) { return null; }
|
||||
|
||||
Vector2 pos = new Vector2(
|
||||
addon->X + (baseNode->X * addon->Scale),
|
||||
addon->Y + (baseNode->Y * addon->Scale)
|
||||
);
|
||||
Vector2 size = new Vector2(
|
||||
imageNode->Width * addon->Scale,
|
||||
imageNode->Height * addon->Scale
|
||||
);
|
||||
|
||||
return new ClipRect(pos, pos + size);
|
||||
}
|
||||
|
||||
private unsafe List<ClipRect> GetHotbarsClipRects()
|
||||
{
|
||||
List<ClipRect> rects = new List<ClipRect>();
|
||||
if (!_config.HotbarsClipRectsEnabled) { return rects; }
|
||||
|
||||
foreach (string addonName in _hotbarAddonNames)
|
||||
{
|
||||
AtkUnitBase* addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName(addonName, 1).Address;
|
||||
if (addon == null || !addon->IsVisible) { continue; }
|
||||
|
||||
AtkComponentNode* firstNode = addon->GetComponentNodeById(8);
|
||||
AtkComponentNode* lastNode = addon->GetComponentNodeById(19);
|
||||
|
||||
if (firstNode == null || lastNode == null) { continue; }
|
||||
|
||||
|
||||
float margin = 10f * addon->Scale;
|
||||
|
||||
Vector2 min = new Vector2(
|
||||
addon->X + (firstNode->AtkResNode.X * addon->Scale) + margin,
|
||||
addon->Y + (firstNode->AtkResNode.Y * addon->Scale) + margin
|
||||
);
|
||||
Vector2 max = new Vector2(
|
||||
addon->X + (lastNode->AtkResNode.X * addon->Scale) + (lastNode->AtkResNode.Width * addon->Scale) - margin,
|
||||
addon->Y + (lastNode->AtkResNode.Y * addon->Scale) + (lastNode->AtkResNode.Height * addon->Scale) - margin
|
||||
);
|
||||
|
||||
rects.Add(new ClipRect(min, max));
|
||||
}
|
||||
|
||||
return rects;
|
||||
}
|
||||
|
||||
private unsafe List<ClipRect> GetNPCChatBubbleClipRect()
|
||||
{
|
||||
List<ClipRect> rects = new List<ClipRect>();
|
||||
if (!_config.ChatBubblesNPCClipRectsEnabled) { return rects; }
|
||||
|
||||
var addon = (AddonMiniTalk*) Plugin.GameGui.GetAddonByName("_MiniTalk").Address;
|
||||
if (addon is null)
|
||||
{
|
||||
return rects;
|
||||
}
|
||||
|
||||
foreach (var talkBubble in addon->TalkBubbles) {
|
||||
if (!talkBubble.ComponentNode->IsVisible())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
AtkNineGridNode* bubbleNineGridNode = talkBubble.BubbleNineGridNode;
|
||||
|
||||
Vector2 position = new Vector2(
|
||||
bubbleNineGridNode->ScreenX,
|
||||
bubbleNineGridNode->ScreenY
|
||||
);
|
||||
Vector2 scale = GetNodeScale((AtkResNode*) bubbleNineGridNode, new Vector2(bubbleNineGridNode->ScaleX, bubbleNineGridNode->ScaleY));
|
||||
Vector2 size = new Vector2(
|
||||
bubbleNineGridNode->Width,
|
||||
bubbleNineGridNode->Height
|
||||
) * scale;
|
||||
|
||||
rects.Add(new ClipRect(position, position + size));
|
||||
}
|
||||
|
||||
return rects;
|
||||
}
|
||||
|
||||
public unsafe List<ClipRect> GetPlayerChatBubbleClipRect()
|
||||
{
|
||||
List<ClipRect> rects = new List<ClipRect>();
|
||||
if (!_config.ChatBubblesPlayersClipRectsEnabled) { return rects; }
|
||||
|
||||
AtkUnitBase* addon = (AtkUnitBase*) Plugin.GameGui.GetAddonByName("MiniTalkPlayer").Address;
|
||||
if (addon is null)
|
||||
{
|
||||
return rects;
|
||||
}
|
||||
|
||||
foreach (var node in addon->UldManager.Nodes) {
|
||||
if (node.Value is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node.Value->GetNodeType() is not NodeType.Component || !node.Value->IsVisible())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
AtkComponentNode* componentNode = (AtkComponentNode*)node.Value;
|
||||
AtkComponentBase* component = componentNode->GetComponent();
|
||||
if (component is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
AtkResNode* bubbleNode = component->UldManager.SearchNodeById(4);
|
||||
if (bubbleNode is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Vector2 position = new Vector2(
|
||||
componentNode->ScreenX,
|
||||
componentNode->ScreenY
|
||||
);
|
||||
Vector2 scale = GetNodeScale(bubbleNode, new Vector2(bubbleNode->ScaleX, bubbleNode->ScaleY));
|
||||
Vector2 size = new Vector2(
|
||||
bubbleNode->Width,
|
||||
bubbleNode->Height
|
||||
) * scale;
|
||||
|
||||
rects.Add(new ClipRect(position, position + size));
|
||||
}
|
||||
|
||||
return rects;
|
||||
}
|
||||
|
||||
public ClipRect? GetClipRectForArea(Vector2 pos, Vector2 size)
|
||||
{
|
||||
if (!_config.Enabled) { return null; }
|
||||
|
||||
List<ClipRect> rects = ActiveClipRects();
|
||||
|
||||
foreach (ClipRect clipRect in rects)
|
||||
{
|
||||
ClipRect area = new ClipRect(pos, pos + size);
|
||||
if (clipRect.IntersectsWith(area))
|
||||
{
|
||||
return clipRect;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static ClipRect[] GetInvertedClipRects(ClipRect clipRect)
|
||||
{
|
||||
float maxX = ImGui.GetMainViewport().Size.X;
|
||||
float maxY = ImGui.GetMainViewport().Size.Y;
|
||||
|
||||
Vector2 aboveMin = new Vector2(0, 0);
|
||||
Vector2 aboveMax = new Vector2(maxX, clipRect.Min.Y);
|
||||
Vector2 leftMin = new Vector2(0, clipRect.Min.Y);
|
||||
Vector2 leftMax = new Vector2(clipRect.Min.X, maxY);
|
||||
|
||||
Vector2 rightMin = new Vector2(clipRect.Max.X, clipRect.Min.Y);
|
||||
Vector2 rightMax = new Vector2(maxX, clipRect.Max.Y);
|
||||
Vector2 belowMin = new Vector2(clipRect.Min.X, clipRect.Max.Y);
|
||||
Vector2 belowMax = new Vector2(maxX, maxY);
|
||||
|
||||
ClipRect[] invertedClipRects = new ClipRect[4];
|
||||
invertedClipRects[0] = new ClipRect(aboveMin, aboveMax);
|
||||
invertedClipRects[1] = new ClipRect(leftMin, leftMax);
|
||||
invertedClipRects[2] = new ClipRect(rightMin, rightMax);
|
||||
invertedClipRects[3] = new ClipRect(belowMin, belowMax);
|
||||
|
||||
return invertedClipRects;
|
||||
}
|
||||
|
||||
public bool IsPointClipped(Vector2 point)
|
||||
{
|
||||
if (!_config.Enabled) { return false; }
|
||||
|
||||
List<ClipRect> rects = ActiveClipRects();
|
||||
|
||||
foreach (ClipRect clipRect in rects)
|
||||
{
|
||||
if (clipRect.Contains(point))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static unsafe Vector2 GetNodeScale(AtkResNode* node, Vector2 currentScale) {
|
||||
if (node is null)
|
||||
{
|
||||
return currentScale;
|
||||
}
|
||||
|
||||
if (node->ParentNode is not null) {
|
||||
currentScale.X *= node->ParentNode->GetScaleX();
|
||||
currentScale.Y *= node->ParentNode->GetScaleY();
|
||||
|
||||
return GetNodeScale(node->ParentNode, currentScale);
|
||||
}
|
||||
|
||||
return currentScale;
|
||||
}
|
||||
}
|
||||
|
||||
public struct ClipRect
|
||||
{
|
||||
public readonly Vector2 Min;
|
||||
public readonly Vector2 Max;
|
||||
|
||||
private readonly Rectangle Rectangle;
|
||||
|
||||
public ClipRect(Vector2 min, Vector2 max)
|
||||
{
|
||||
Vector2 screenSize = ImGui.GetMainViewport().Size;
|
||||
|
||||
Min = Clamp(min, Vector2.Zero, screenSize);
|
||||
Max = Clamp(max, Vector2.Zero, screenSize);
|
||||
|
||||
Vector2 size = Max - Min;
|
||||
|
||||
Rectangle = new Rectangle((int)Min.X, (int)Min.Y, (int)size.X, (int)size.Y);
|
||||
}
|
||||
|
||||
public bool Contains(Vector2 point)
|
||||
{
|
||||
return Rectangle.Contains((int)point.X, (int)point.Y);
|
||||
}
|
||||
|
||||
public bool IntersectsWith(ClipRect other)
|
||||
{
|
||||
return Rectangle.IntersectsWith(other.Rectangle);
|
||||
}
|
||||
|
||||
public ClipRect? Intersect(ClipRect other)
|
||||
{
|
||||
float minX = Math.Max(Min.X, other.Min.X);
|
||||
float minY = Math.Max(Min.Y, other.Min.Y);
|
||||
float maxX = Math.Min(Max.X, other.Max.X);
|
||||
float maxY = Math.Min(Max.Y, other.Max.Y);
|
||||
if (minX >= maxX || minY >= maxY) return null;
|
||||
return new ClipRect(new Vector2(minX, minY), new Vector2(maxX, maxY));
|
||||
}
|
||||
|
||||
private static Vector2 Clamp(Vector2 vector, Vector2 min, Vector2 max)
|
||||
{
|
||||
return new Vector2(Math.Max(min.X, Math.Min(max.X, vector.X)), Math.Max(min.Y, Math.Min(max.Y, vector.Y)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
using Colourful;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using HSUI.Config;
|
||||
using HSUI.Enums;
|
||||
using HSUI.Interface.GeneralElements;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace HSUI.Helpers
|
||||
{
|
||||
public static class ColorUtils
|
||||
{
|
||||
|
||||
//Build our converter objects and store them in a field. This will be used to convert our PluginConfigColors into different color spaces to be used for interpolation
|
||||
private static readonly IColorConverter<RGBColor, LabColor> _rgbToLab = new ConverterBuilder().FromRGB().ToLab().Build();
|
||||
private static readonly IColorConverter<LabColor, RGBColor> _labToRgb = new ConverterBuilder().FromLab().ToRGB().Build();
|
||||
|
||||
private static readonly IColorConverter<RGBColor, LChabColor> _rgbToLChab = new ConverterBuilder().FromRGB().ToLChab().Build();
|
||||
private static readonly IColorConverter<LChabColor, RGBColor> _lchabToRgb = new ConverterBuilder().FromLChab().ToRGB().Build();
|
||||
|
||||
private static readonly IColorConverter<RGBColor, XYZColor> _rgbToXyz = new ConverterBuilder().FromRGB(RGBWorkingSpaces.sRGB).ToXYZ(Illuminants.D65).Build();
|
||||
private static readonly IColorConverter<XYZColor, RGBColor> _xyzToRgb = new ConverterBuilder().FromXYZ(Illuminants.D65).ToRGB(RGBWorkingSpaces.sRGB).Build();
|
||||
|
||||
private static readonly IColorConverter<RGBColor, LChuvColor> _rgbToLChuv = new ConverterBuilder().FromRGB().ToLChuv().Build();
|
||||
private static readonly IColorConverter<LChuvColor, RGBColor> _lchuvToRgb = new ConverterBuilder().FromLChuv().ToRGB().Build();
|
||||
|
||||
private static readonly IColorConverter<RGBColor, LuvColor> _rgbToLuv = new ConverterBuilder().FromRGB().ToLuv().Build();
|
||||
private static readonly IColorConverter<LuvColor, RGBColor> _luvToRgb = new ConverterBuilder().FromLuv().ToRGB().Build();
|
||||
|
||||
private static readonly IColorConverter<RGBColor, JzazbzColor> _rgbToJzazbz = new ConverterBuilder().FromRGB().ToJzazbz().Build();
|
||||
private static readonly IColorConverter<JzazbzColor, RGBColor> _jzazbzToRgb = new ConverterBuilder().FromJzazbz().ToRGB().Build();
|
||||
|
||||
private static readonly IColorConverter<RGBColor, JzCzhzColor> _rgbToJzCzhz = new ConverterBuilder().FromRGB().ToJzCzhz().Build();
|
||||
private static readonly IColorConverter<JzCzhzColor, RGBColor> _jzCzhzToRgb = new ConverterBuilder().FromJzCzhz().ToRGB().Build();
|
||||
|
||||
//Simple LinearInterpolation method. T = [0 , 1]
|
||||
private static float LinearInterpolation(float left, float right, float t)
|
||||
=> left + ((right - left) * t);
|
||||
|
||||
public static PluginConfigColor GetColorByScale(float i, ColorByHealthValueConfig config) =>
|
||||
GetColorByScale(i, config.LowHealthColorThreshold / 100f, config.FullHealthColorThreshold / 100f, config.LowHealthColor, config.FullHealthColor, config.MaxHealthColor, config.UseMaxHealthColor, config.BlendMode);
|
||||
|
||||
//Method used to interpolate two PluginConfigColors
|
||||
//i is scale [0 , 1]
|
||||
//min and max are used for color thresholds. for instance return colorLeft if i < min or return ColorRight if i > max
|
||||
public static PluginConfigColor GetColorByScale(float i, float min, float max, PluginConfigColor colorLeft, PluginConfigColor colorRight, PluginConfigColor colorMax, bool useMaxColor, BlendMode blendMode)
|
||||
{
|
||||
//Set our thresholds where the ratio is the range of values we will use for interpolation.
|
||||
//Values outside this range will either return colorLeft or colorRight
|
||||
float ratio = i;
|
||||
if (min > 0 || max < 1)
|
||||
{
|
||||
if (i < min)
|
||||
{
|
||||
ratio = 0;
|
||||
}
|
||||
else if (i > max)
|
||||
{
|
||||
ratio = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
float range = max - min;
|
||||
ratio = (i - min) / range;
|
||||
}
|
||||
}
|
||||
|
||||
//Convert our PluginConfigColor to RGBColor
|
||||
RGBColor rgbColorLeft = new RGBColor(colorLeft.Vector.X, colorLeft.Vector.Y, colorLeft.Vector.Z);
|
||||
RGBColor rgbColorRight = new RGBColor(colorRight.Vector.X, colorRight.Vector.Y, colorRight.Vector.Z);
|
||||
|
||||
//Interpolate our Alpha now
|
||||
float alpha = LinearInterpolation(colorLeft.Vector.W, colorRight.Vector.W, ratio);
|
||||
|
||||
if (ratio >= 1 && useMaxColor)
|
||||
{
|
||||
return new PluginConfigColor(new Vector4((float)colorMax.Vector.X, (float)colorMax.Vector.Y, (float)colorMax.Vector.Z, colorMax.Vector.W));
|
||||
}
|
||||
|
||||
//Allow the users to select different blend modes since interpolating between two colors can result in different blending depending on the color space
|
||||
//We convert our RGBColor values into different color spaces. We then interpolate each channel before converting the color back into RGBColor space
|
||||
switch (blendMode)
|
||||
{
|
||||
case BlendMode.LAB:
|
||||
{
|
||||
//convert RGB to LAB
|
||||
LabColor LabLeft = _rgbToLab.Convert(rgbColorLeft);
|
||||
LabColor LabRight = _rgbToLab.Convert(rgbColorRight);
|
||||
|
||||
RGBColor Lab2RGB = _labToRgb.Convert(
|
||||
new LabColor(
|
||||
LinearInterpolation((float)LabLeft.L, (float)LabRight.L, ratio),
|
||||
LinearInterpolation((float)LabLeft.a, (float)LabRight.a, ratio),
|
||||
LinearInterpolation((float)LabLeft.b, (float)LabRight.b, ratio)
|
||||
)
|
||||
);
|
||||
|
||||
Lab2RGB.NormalizeIntensity();
|
||||
return new PluginConfigColor(new Vector4((float)Lab2RGB.R, (float)Lab2RGB.G, (float)Lab2RGB.B, alpha));
|
||||
}
|
||||
|
||||
case BlendMode.LChab:
|
||||
{
|
||||
//convert RGB to LChab
|
||||
LChabColor LChabLeft = _rgbToLChab.Convert(rgbColorLeft);
|
||||
LChabColor LChabRight = _rgbToLChab.Convert(rgbColorRight);
|
||||
|
||||
RGBColor LChab2RGB = _lchabToRgb.Convert(
|
||||
new LChabColor(
|
||||
LinearInterpolation((float)LChabLeft.L, (float)LChabRight.L, ratio),
|
||||
LinearInterpolation((float)LChabLeft.C, (float)LChabRight.C, ratio),
|
||||
LinearInterpolation((float)LChabLeft.h, (float)LChabRight.h, ratio)
|
||||
)
|
||||
);
|
||||
|
||||
LChab2RGB.NormalizeIntensity();
|
||||
|
||||
return new PluginConfigColor(new Vector4((float)LChab2RGB.R, (float)LChab2RGB.G, (float)LChab2RGB.B, alpha));
|
||||
}
|
||||
case BlendMode.XYZ:
|
||||
{
|
||||
//convert RGB to XYZ
|
||||
XYZColor XYZLeft = _rgbToXyz.Convert(rgbColorLeft);
|
||||
XYZColor XYZRight = _rgbToXyz.Convert(rgbColorRight);
|
||||
|
||||
RGBColor XYZ2RGB = _xyzToRgb.Convert(
|
||||
new XYZColor(
|
||||
LinearInterpolation((float)XYZLeft.X, (float)XYZRight.X, ratio),
|
||||
LinearInterpolation((float)XYZLeft.Y, (float)XYZRight.Y, ratio),
|
||||
LinearInterpolation((float)XYZLeft.Z, (float)XYZRight.Z, ratio)
|
||||
)
|
||||
);
|
||||
|
||||
XYZ2RGB.NormalizeIntensity();
|
||||
|
||||
return new PluginConfigColor(new Vector4((float)XYZ2RGB.R, (float)XYZ2RGB.G, (float)XYZ2RGB.B, alpha));
|
||||
}
|
||||
case BlendMode.RGB:
|
||||
{
|
||||
//No conversion needed here because we are already working in RGB space
|
||||
RGBColor newRGB = new RGBColor(
|
||||
LinearInterpolation((float)rgbColorLeft.R, (float)rgbColorRight.R, ratio),
|
||||
LinearInterpolation((float)rgbColorLeft.G, (float)rgbColorRight.G, ratio),
|
||||
LinearInterpolation((float)rgbColorLeft.B, (float)rgbColorRight.B, ratio)
|
||||
);
|
||||
|
||||
return new PluginConfigColor(new Vector4((float)newRGB.R, (float)newRGB.G, (float)newRGB.B, alpha));
|
||||
}
|
||||
case BlendMode.LChuv:
|
||||
{
|
||||
//convert RGB to LChuv
|
||||
LChuvColor LChuvLeft = _rgbToLChuv.Convert(rgbColorLeft);
|
||||
LChuvColor LChuvRight = _rgbToLChuv.Convert(rgbColorRight);
|
||||
|
||||
RGBColor LChuv2RGB = _lchuvToRgb.Convert(
|
||||
new LChuvColor(
|
||||
LinearInterpolation((float)LChuvLeft.L, (float)LChuvRight.L, ratio),
|
||||
LinearInterpolation((float)LChuvLeft.C, (float)LChuvRight.C, ratio),
|
||||
LinearInterpolation((float)LChuvLeft.h, (float)LChuvRight.h, ratio)
|
||||
)
|
||||
);
|
||||
|
||||
LChuv2RGB.NormalizeIntensity();
|
||||
|
||||
return new PluginConfigColor(new Vector4((float)LChuv2RGB.R, (float)LChuv2RGB.G, (float)LChuv2RGB.B, alpha));
|
||||
}
|
||||
|
||||
case BlendMode.Luv:
|
||||
{
|
||||
//convert RGB to Luv
|
||||
LuvColor LuvLeft = _rgbToLuv.Convert(rgbColorLeft);
|
||||
LuvColor LuvRight = _rgbToLuv.Convert(rgbColorRight);
|
||||
|
||||
RGBColor Luv2RGB = _luvToRgb.Convert(
|
||||
new LuvColor(
|
||||
LinearInterpolation((float)LuvLeft.L, (float)LuvRight.L, ratio),
|
||||
LinearInterpolation((float)LuvLeft.u, (float)LuvRight.u, ratio),
|
||||
LinearInterpolation((float)LuvLeft.v, (float)LuvRight.v, ratio)
|
||||
)
|
||||
);
|
||||
|
||||
Luv2RGB.NormalizeIntensity();
|
||||
|
||||
return new PluginConfigColor(new Vector4((float)Luv2RGB.R, (float)Luv2RGB.G, (float)Luv2RGB.B, alpha));
|
||||
|
||||
}
|
||||
case BlendMode.Jzazbz:
|
||||
{
|
||||
//convert RGB to Jzazbz
|
||||
JzazbzColor JzazbzLeft = _rgbToJzazbz.Convert(rgbColorLeft);
|
||||
JzazbzColor JzazbzRight = _rgbToJzazbz.Convert(rgbColorRight);
|
||||
|
||||
RGBColor Jzazbz2RGB = _jzazbzToRgb.Convert(
|
||||
new JzazbzColor(
|
||||
LinearInterpolation((float)JzazbzLeft.Jz, (float)JzazbzRight.Jz, ratio),
|
||||
LinearInterpolation((float)JzazbzLeft.az, (float)JzazbzRight.az, ratio),
|
||||
LinearInterpolation((float)JzazbzLeft.bz, (float)JzazbzRight.bz, ratio)
|
||||
)
|
||||
);
|
||||
|
||||
Jzazbz2RGB.NormalizeIntensity();
|
||||
|
||||
return new PluginConfigColor(new Vector4((float)Jzazbz2RGB.R, (float)Jzazbz2RGB.G, (float)Jzazbz2RGB.B, alpha));
|
||||
}
|
||||
case BlendMode.JzCzhz:
|
||||
{
|
||||
//convert RGB to JzCzhz
|
||||
JzCzhzColor JzCzhzLeft = _rgbToJzCzhz.Convert(rgbColorLeft);
|
||||
JzCzhzColor JzCzhzRight = _rgbToJzCzhz.Convert(rgbColorRight);
|
||||
|
||||
RGBColor JzCzhz2RGB = _jzCzhzToRgb.Convert(
|
||||
new JzCzhzColor(
|
||||
LinearInterpolation((float)JzCzhzLeft.Jz, (float)JzCzhzRight.Jz, ratio),
|
||||
LinearInterpolation((float)JzCzhzLeft.Cz, (float)JzCzhzRight.Cz, ratio),
|
||||
LinearInterpolation((float)JzCzhzLeft.hz, (float)JzCzhzRight.hz, ratio)
|
||||
)
|
||||
);
|
||||
|
||||
JzCzhz2RGB.NormalizeIntensity();
|
||||
|
||||
return new PluginConfigColor(new Vector4((float)JzCzhz2RGB.R, (float)JzCzhz2RGB.G, (float)JzCzhz2RGB.B, alpha));
|
||||
}
|
||||
}
|
||||
return new(Vector4.One);
|
||||
}
|
||||
|
||||
public static PluginConfigColor ColorForActor(IGameObject? actor)
|
||||
{
|
||||
if (actor == null || actor is not ICharacter character)
|
||||
{
|
||||
return GlobalColors.Instance.NPCNeutralColor;
|
||||
}
|
||||
|
||||
if (character.ObjectKind == ObjectKind.Player ||
|
||||
character.SubKind == 9 && character.ClassJob.RowId > 0)
|
||||
{
|
||||
return GlobalColors.Instance.SafeColorForJobId(character.ClassJob.RowId);
|
||||
}
|
||||
|
||||
bool isHostile = Utils.IsHostile(character);
|
||||
|
||||
if (character is IBattleNpc npc)
|
||||
{
|
||||
if ((npc.BattleNpcKind == BattleNpcSubKind.Enemy || npc.BattleNpcKind == BattleNpcSubKind.BattleNpcPart) && isHostile)
|
||||
{
|
||||
return GlobalColors.Instance.NPCHostileColor;
|
||||
}
|
||||
else
|
||||
{
|
||||
return GlobalColors.Instance.NPCFriendlyColor;
|
||||
}
|
||||
}
|
||||
|
||||
return isHostile ? GlobalColors.Instance.NPCNeutralColor : GlobalColors.Instance.NPCFriendlyColor;
|
||||
}
|
||||
|
||||
public static PluginConfigColor? ColorForCharacter(
|
||||
IGameObject? gameObject,
|
||||
uint currentHp = 0,
|
||||
uint maxHp = 0,
|
||||
bool useJobColor = false,
|
||||
bool useRoleColor = false,
|
||||
ColorByHealthValueConfig? colorByHealthConfig = null)
|
||||
{
|
||||
ICharacter? character = gameObject as ICharacter;
|
||||
|
||||
if (useJobColor && character != null)
|
||||
{
|
||||
return ColorForActor(character);
|
||||
}
|
||||
else if (useRoleColor)
|
||||
{
|
||||
return character is IPlayerCharacter ?
|
||||
GlobalColors.Instance.SafeRoleColorForJobId(character.ClassJob.RowId) :
|
||||
ColorForActor(character);
|
||||
}
|
||||
else if (colorByHealthConfig != null && colorByHealthConfig.Enabled && character != null)
|
||||
{
|
||||
var scale = (float)currentHp / Math.Max(1, maxHp);
|
||||
if (colorByHealthConfig.UseJobColorAsMaxHealth)
|
||||
{
|
||||
return GetColorByScale(
|
||||
scale,
|
||||
colorByHealthConfig.LowHealthColorThreshold / 100f,
|
||||
colorByHealthConfig.FullHealthColorThreshold / 100f,
|
||||
colorByHealthConfig.LowHealthColor,
|
||||
colorByHealthConfig.FullHealthColor,
|
||||
ColorForActor(character),
|
||||
colorByHealthConfig.UseMaxHealthColor,
|
||||
colorByHealthConfig.BlendMode
|
||||
);
|
||||
}
|
||||
else if (colorByHealthConfig.UseRoleColorAsMaxHealth)
|
||||
{
|
||||
return GetColorByScale(scale,
|
||||
colorByHealthConfig.LowHealthColorThreshold / 100f,
|
||||
colorByHealthConfig.FullHealthColorThreshold / 100f,
|
||||
colorByHealthConfig.LowHealthColor, colorByHealthConfig.FullHealthColor,
|
||||
character is IPlayerCharacter ? GlobalColors.Instance.SafeRoleColorForJobId(character.ClassJob.RowId) : ColorForActor(character),
|
||||
colorByHealthConfig.UseMaxHealthColor,
|
||||
colorByHealthConfig.BlendMode
|
||||
);
|
||||
}
|
||||
return GetColorByScale(scale, colorByHealthConfig);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
using HSUI.Config;
|
||||
using HSUI.Enums;
|
||||
using HSUI.Interface;
|
||||
using HSUI.Interface.GeneralElements;
|
||||
using HSUI.Interface.Jobs;
|
||||
using HSUI.Interface.StatusEffects;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
namespace HSUI.Helpers
|
||||
{
|
||||
public static class DraggablesHelper
|
||||
{
|
||||
public static void DrawGrid(GridConfig config, HUDOptionsConfig? hudConfig, DraggableHudElement? selectedElement)
|
||||
{
|
||||
ImGui.SetNextWindowPos(Vector2.Zero);
|
||||
ImGui.SetNextWindowSize(ImGui.GetMainViewport().Size);
|
||||
|
||||
ImGui.SetNextWindowBgAlpha(config.BackgroundAlpha);
|
||||
|
||||
ImGui.Begin("DelvUI_grid",
|
||||
ImGuiWindowFlags.NoTitleBar
|
||||
| ImGuiWindowFlags.NoScrollbar
|
||||
| ImGuiWindowFlags.AlwaysAutoResize
|
||||
| ImGuiWindowFlags.NoInputs
|
||||
| ImGuiWindowFlags.NoDecoration
|
||||
| ImGuiWindowFlags.NoBringToFrontOnFocus
|
||||
| ImGuiWindowFlags.NoFocusOnAppearing
|
||||
);
|
||||
|
||||
ImDrawListPtr drawList = ImGui.GetWindowDrawList();
|
||||
Vector2 screenSize = ImGui.GetMainViewport().Size;
|
||||
Vector2 offset = hudConfig != null && hudConfig.UseGlobalHudShift ? hudConfig.HudOffset : Vector2.Zero;
|
||||
Vector2 center = screenSize / 2f + offset;
|
||||
|
||||
// grid
|
||||
if (config.ShowGrid)
|
||||
{
|
||||
int count = (int)(Math.Max(screenSize.X, screenSize.Y) / config.GridDivisionsDistance) / 2 + 1;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var step = i * config.GridDivisionsDistance;
|
||||
|
||||
drawList.AddLine(new Vector2(center.X + step, 0), new Vector2(center.X + step, screenSize.Y), 0x88888888);
|
||||
drawList.AddLine(new Vector2(center.X - step, 0), new Vector2(center.X - step, screenSize.Y), 0x88888888);
|
||||
|
||||
drawList.AddLine(new Vector2(0, center.Y + step), new Vector2(screenSize.X, center.Y + step), 0x88888888);
|
||||
drawList.AddLine(new Vector2(0, center.Y - step), new Vector2(screenSize.X, center.Y - step), 0x88888888);
|
||||
|
||||
if (config.GridSubdivisionCount > 1)
|
||||
{
|
||||
for (int j = 1; j < config.GridSubdivisionCount; j++)
|
||||
{
|
||||
var subStep = j * (config.GridDivisionsDistance / config.GridSubdivisionCount);
|
||||
|
||||
drawList.AddLine(new Vector2(center.X + step + subStep, 0), new Vector2(center.X + step + subStep, screenSize.Y), 0x44888888);
|
||||
drawList.AddLine(new Vector2(center.X - step - subStep, 0), new Vector2(center.X - step - subStep, screenSize.Y), 0x44888888);
|
||||
|
||||
drawList.AddLine(new Vector2(0, center.Y + step + subStep), new Vector2(screenSize.X, center.Y + step + subStep), 0x44888888);
|
||||
drawList.AddLine(new Vector2(0, center.Y - step - subStep), new Vector2(screenSize.X, center.Y - step - subStep), 0x44888888);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// center lines
|
||||
if (config.ShowCenterLines)
|
||||
{
|
||||
drawList.AddLine(new Vector2(center.X, 0), new Vector2(center.X, screenSize.Y), 0xAAFFFFFF);
|
||||
drawList.AddLine(new Vector2(0, center.Y), new Vector2(screenSize.X, center.Y), 0xAAFFFFFF);
|
||||
}
|
||||
|
||||
if (config.ShowAnchorPoints && selectedElement != null)
|
||||
{
|
||||
Vector2 parentAnchorPos = center + selectedElement.ParentPos();
|
||||
Vector2 anchorPos = parentAnchorPos + selectedElement.GetConfig().Position;
|
||||
|
||||
drawList.AddLine(parentAnchorPos, anchorPos, 0xAA0000FF, 2);
|
||||
|
||||
var anchorSize = new Vector2(10, 10);
|
||||
drawList.AddRectFilled(anchorPos - anchorSize / 2f, anchorPos + anchorSize / 2f, 0xAA0000FF);
|
||||
}
|
||||
|
||||
ImGui.End();
|
||||
}
|
||||
|
||||
public static void DrawElements(
|
||||
Vector2 origin,
|
||||
HudHelper hudHelper,
|
||||
IList<DraggableHudElement> elements,
|
||||
JobHud? jobHud,
|
||||
DraggableHudElement? selectedElement)
|
||||
{
|
||||
foreach (DraggableHudElement element in elements)
|
||||
{
|
||||
if (!hudHelper.IsElementHidden(element))
|
||||
{
|
||||
element.PrepareForDraw(origin);
|
||||
}
|
||||
}
|
||||
|
||||
jobHud?.PrepareForDraw(origin);
|
||||
|
||||
bool clip = ConfigurationManager.Instance?.LockHUD == true &&
|
||||
ClipRectsHelper.Instance?.Enabled == true &&
|
||||
ClipRectsHelper.Instance?.Mode == WindowClippingMode.Performance;
|
||||
|
||||
bool needsDraw = true;
|
||||
|
||||
if (clip)
|
||||
{
|
||||
ClipRect? clipRect = ClipRectsHelper.Instance?.GetClipRectForArea(Vector2.Zero, ImGui.GetMainViewport().Size);
|
||||
if (clipRect.HasValue)
|
||||
{
|
||||
needsDraw = false;
|
||||
|
||||
ClipRect[] invertedClipRects = ClipRectsHelper.GetInvertedClipRects(clipRect.Value);
|
||||
for (int i = 0; i < invertedClipRects.Length; i++)
|
||||
{
|
||||
ImGui.PushClipRect(invertedClipRects[i].Min, invertedClipRects[i].Max, false);
|
||||
Draw(origin, hudHelper, elements, jobHud, selectedElement);
|
||||
ImGui.PopClipRect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needsDraw)
|
||||
{
|
||||
Draw(origin, hudHelper, elements, jobHud, selectedElement);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Draw(
|
||||
Vector2 origin,
|
||||
HudHelper hudHelper,
|
||||
IList<DraggableHudElement> elements,
|
||||
JobHud? jobHud,
|
||||
DraggableHudElement? selectedElement)
|
||||
{
|
||||
bool canTakeInput = true;
|
||||
bool jobHudNeedsDraw = jobHud != null && jobHud != selectedElement && !hudHelper.IsElementHidden(jobHud);
|
||||
|
||||
// selected
|
||||
if (selectedElement != null)
|
||||
{
|
||||
if (!hudHelper.IsElementHidden(selectedElement))
|
||||
{
|
||||
selectedElement.CanTakeInputForDrag = true;
|
||||
selectedElement.Draw(origin);
|
||||
canTakeInput = !selectedElement.NeedsInputForDrag;
|
||||
}
|
||||
else if (selectedElement is IHudElementWithMouseOver elementWithMouseOver)
|
||||
{
|
||||
elementWithMouseOver.StopMouseover();
|
||||
}
|
||||
}
|
||||
|
||||
// all
|
||||
foreach (DraggableHudElement element in elements)
|
||||
{
|
||||
if (element == selectedElement) { continue; }
|
||||
|
||||
if (jobHudNeedsDraw && jobHud != null && element.GetConfig().StrataLevel > jobHud.GetConfig().StrataLevel)
|
||||
{
|
||||
jobHud.CanTakeInputForDrag = canTakeInput;
|
||||
jobHud.Draw(origin);
|
||||
jobHudNeedsDraw = false;
|
||||
}
|
||||
|
||||
if (!hudHelper.IsElementHidden(element))
|
||||
{
|
||||
element.CanTakeInputForDrag = canTakeInput;
|
||||
element.Draw(origin);
|
||||
canTakeInput = !canTakeInput ? false : !element.NeedsInputForDrag;
|
||||
}
|
||||
else if (element is IHudElementWithMouseOver elementWithMouseOver)
|
||||
{
|
||||
elementWithMouseOver.StopMouseover();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool DrawArrows(Vector2 position, Vector2 size, string tooltipText, out Vector2 offset)
|
||||
{
|
||||
offset = Vector2.Zero;
|
||||
|
||||
var windowFlags = ImGuiWindowFlags.NoScrollbar
|
||||
| ImGuiWindowFlags.NoTitleBar
|
||||
| ImGuiWindowFlags.NoResize
|
||||
| ImGuiWindowFlags.NoBackground
|
||||
| ImGuiWindowFlags.NoDecoration
|
||||
| ImGuiWindowFlags.NoSavedSettings;
|
||||
|
||||
var margin = new Vector2(4, 0);
|
||||
var windowSize = ArrowSize + margin * 2;
|
||||
|
||||
// left, right, up, down
|
||||
var positions = GetArrowPositions(position, size);
|
||||
var offsets = new Vector2[]
|
||||
{
|
||||
new Vector2(-1, 0),
|
||||
new Vector2(1, 0),
|
||||
new Vector2(0, -1),
|
||||
new Vector2(0, 1)
|
||||
};
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
var pos = positions[i] - margin;
|
||||
|
||||
ImGui.SetNextWindowSize(windowSize, ImGuiCond.Always);
|
||||
ImGui.SetNextWindowPos(pos);
|
||||
|
||||
ImGui.Begin("DelvUI_draggablesArrow " + i.ToString(), windowFlags);
|
||||
|
||||
// fake button
|
||||
ImGuiP.ArrowButtonEx($"arrow button {i}", (ImGuiDir)i, new Vector2(ArrowSize.X, ArrowSize.Y));
|
||||
if (ImGui.IsMouseHoveringRect(pos, pos + windowSize))
|
||||
{
|
||||
// track click manually to not deal with window focus stuff
|
||||
if (ImGui.IsMouseClicked(ImGuiMouseButton.Left))
|
||||
{
|
||||
offset = offsets[i];
|
||||
}
|
||||
|
||||
// tooltip
|
||||
TooltipsHelper.Instance.ShowTooltipOnCursor(tooltipText);
|
||||
}
|
||||
|
||||
ImGui.End();
|
||||
}
|
||||
|
||||
return offset != Vector2.Zero;
|
||||
}
|
||||
|
||||
public static Vector2 ArrowSize = new Vector2(40, 40);
|
||||
|
||||
public static Vector2[] GetArrowPositions(Vector2 position, Vector2 size)
|
||||
{
|
||||
return GetArrowPositions(position, size, ArrowSize);
|
||||
}
|
||||
|
||||
public static Vector2[] GetArrowPositions(Vector2 position, Vector2 size, Vector2 arrowSize)
|
||||
{
|
||||
return new Vector2[]
|
||||
{
|
||||
new Vector2(position.X - arrowSize.X + 10, position.Y + size.Y / 2f - arrowSize.Y / 2f - 2),
|
||||
new Vector2(position.X + size.X - 8, position.Y + size.Y / 2f - arrowSize.Y / 2f - 2),
|
||||
new Vector2(position.X + size.X / 2f - arrowSize.X / 2f + 2, position.Y - arrowSize.Y + 1),
|
||||
new Vector2(position.X + size.X / 2f - arrowSize.X / 2f + 2, position.Y + size.Y - 7)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
using Dalamud.Interface.Textures.TextureWraps;
|
||||
using Dalamud.Interface.Utility;
|
||||
using HSUI.Config;
|
||||
using HSUI.Enums;
|
||||
using HSUI.Interface.GeneralElements;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Lumina.Excel;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace HSUI.Helpers
|
||||
{
|
||||
public enum GradientDirection
|
||||
{
|
||||
None,
|
||||
Right,
|
||||
Left,
|
||||
Up,
|
||||
Down,
|
||||
CenteredHorizonal
|
||||
}
|
||||
|
||||
public static class DrawHelper
|
||||
{
|
||||
private static uint[] ColorArray(PluginConfigColor color, GradientDirection gradientDirection)
|
||||
{
|
||||
return gradientDirection switch
|
||||
{
|
||||
GradientDirection.None => new[] { color.Base, color.Base, color.Base, color.Base },
|
||||
GradientDirection.Right => new[] { color.TopGradient, color.BottomGradient, color.BottomGradient, color.TopGradient },
|
||||
GradientDirection.Left => new[] { color.BottomGradient, color.TopGradient, color.TopGradient, color.BottomGradient },
|
||||
GradientDirection.Up => new[] { color.BottomGradient, color.BottomGradient, color.TopGradient, color.TopGradient },
|
||||
_ => new[] { color.TopGradient, color.TopGradient, color.BottomGradient, color.BottomGradient }
|
||||
};
|
||||
}
|
||||
|
||||
private static Vector2 GetBarTextureUV1Vector(Vector2 size, int textureWidth, int textureHeight, BarTextureDrawMode drawMode)
|
||||
{
|
||||
if (drawMode == BarTextureDrawMode.Stretch) { return new Vector2(1); }
|
||||
|
||||
float x = drawMode == BarTextureDrawMode.RepeatVertical ? 1 : (float)size.X / textureWidth;
|
||||
float y = drawMode == BarTextureDrawMode.RepeatHorizontal ? 1 : (float)size.Y / textureHeight;
|
||||
|
||||
return new Vector2(x, y);
|
||||
}
|
||||
|
||||
public static void DrawBarTexture(Vector2 position, Vector2 size, PluginConfigColor color, string? name, BarTextureDrawMode drawMode, ImDrawListPtr drawList)
|
||||
{
|
||||
IDalamudTextureWrap? texture = BarTexturesManager.Instance?.GetBarTexture(name);
|
||||
if (texture == null)
|
||||
{
|
||||
DrawGradientFilledRect(position, size, color, drawList);
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 uv0 = new Vector2(0);
|
||||
Vector2 uv1 = GetBarTextureUV1Vector(size, texture.Width, texture.Height, drawMode);
|
||||
|
||||
drawList.AddImage(texture.Handle, position, position + size, uv0, uv1, color.Base);
|
||||
}
|
||||
|
||||
public static void DrawGradientFilledRect(Vector2 position, Vector2 size, PluginConfigColor color, ImDrawListPtr drawList)
|
||||
{
|
||||
GradientDirection gradientDirection = ConfigurationManager.Instance.GradientDirection;
|
||||
DrawGradientFilledRect(position, size, color, drawList, gradientDirection);
|
||||
}
|
||||
|
||||
public static void DrawGradientFilledRect(Vector2 position, Vector2 size, PluginConfigColor color, ImDrawListPtr drawList, GradientDirection gradientDirection = GradientDirection.Down)
|
||||
{
|
||||
uint[]? colorArray = ColorArray(color, gradientDirection);
|
||||
|
||||
if (gradientDirection == GradientDirection.CenteredHorizonal)
|
||||
{
|
||||
Vector2 halfSize = new(size.X, size.Y / 2f);
|
||||
drawList.AddRectFilledMultiColor(
|
||||
position, position + halfSize,
|
||||
colorArray[0], colorArray[1], colorArray[2], colorArray[3]
|
||||
);
|
||||
|
||||
Vector2 pos = position + new Vector2(0, halfSize.Y);
|
||||
drawList.AddRectFilledMultiColor(
|
||||
pos, pos + halfSize,
|
||||
colorArray[3], colorArray[2], colorArray[1], colorArray[0]
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
drawList.AddRectFilledMultiColor(
|
||||
position, position + size,
|
||||
colorArray[0], colorArray[1], colorArray[2], colorArray[3]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static void DrawOutlinedText(string text, Vector2 pos, ImDrawListPtr drawList, int thickness = 1)
|
||||
{
|
||||
DrawOutlinedText(text, pos, 0xFFFFFFFF, 0xFF000000, drawList, thickness);
|
||||
}
|
||||
|
||||
public static void DrawOutlinedText(string text, Vector2 pos, uint color, uint outlineColor, ImDrawListPtr drawList, int thickness = 1)
|
||||
{
|
||||
// outline
|
||||
for (int i = 1; i < thickness + 1; i++)
|
||||
{
|
||||
drawList.AddText(new Vector2(pos.X - i, pos.Y + i), outlineColor, text);
|
||||
drawList.AddText(new Vector2(pos.X, pos.Y + i), outlineColor, text);
|
||||
drawList.AddText(new Vector2(pos.X + i, pos.Y + i), outlineColor, text);
|
||||
drawList.AddText(new Vector2(pos.X - i, pos.Y), outlineColor, text);
|
||||
drawList.AddText(new Vector2(pos.X + i, pos.Y), outlineColor, text);
|
||||
drawList.AddText(new Vector2(pos.X - i, pos.Y - i), outlineColor, text);
|
||||
drawList.AddText(new Vector2(pos.X, pos.Y - i), outlineColor, text);
|
||||
drawList.AddText(new Vector2(pos.X + i, pos.Y - i), outlineColor, text);
|
||||
}
|
||||
|
||||
// text
|
||||
drawList.AddText(new Vector2(pos.X, pos.Y), color, text);
|
||||
}
|
||||
|
||||
public static void DrawShadowText(string text, Vector2 pos, uint color, uint shadowColor, ImDrawListPtr drawList, int offset = 1, int thickness = 1)
|
||||
{
|
||||
// TODO: Add parameter to allow to choose a direction
|
||||
|
||||
// Shadow
|
||||
for (int i = 0; i < thickness; i++)
|
||||
{
|
||||
drawList.AddText(new Vector2(pos.X + i + offset, pos.Y + i + offset), shadowColor, text);
|
||||
}
|
||||
|
||||
// Text
|
||||
drawList.AddText(new Vector2(pos.X, pos.Y), color, text);
|
||||
}
|
||||
|
||||
public static void DrawIcon<T>(dynamic row, Vector2 position, Vector2 size, bool drawBorder, bool cropIcon, int stackCount = 1) where T : struct, IExcelRow<T>
|
||||
{
|
||||
IDalamudTextureWrap texture = TexturesHelper.GetTexture<T>(row, (uint)Math.Max(0, stackCount - 1));
|
||||
if (texture == null) { return; }
|
||||
|
||||
(Vector2 uv0, Vector2 uv1) = GetTexCoordinates(texture, size, cropIcon);
|
||||
|
||||
ImGui.SetCursorPos(position);
|
||||
ImGui.Image(texture.Handle, size, uv0, uv1);
|
||||
|
||||
if (drawBorder)
|
||||
{
|
||||
ImDrawListPtr drawList = ImGui.GetWindowDrawList();
|
||||
drawList.AddRect(position, position + size, 0xFF000000);
|
||||
}
|
||||
}
|
||||
|
||||
public static void DrawIcon<T>(ImDrawListPtr drawList, dynamic row, Vector2 position, Vector2 size, bool drawBorder, bool cropIcon, int stackCount = 1) where T : struct, IExcelRow<T>
|
||||
{
|
||||
IDalamudTextureWrap texture = TexturesHelper.GetTexture<T>(row, (uint)Math.Max(0, stackCount - 1));
|
||||
if (texture == null) { return; }
|
||||
|
||||
(Vector2 uv0, Vector2 uv1) = GetTexCoordinates(texture, size, cropIcon);
|
||||
|
||||
drawList.AddImage(texture.Handle, position, position + size, uv0, uv1);
|
||||
|
||||
if (drawBorder)
|
||||
{
|
||||
drawList.AddRect(position, position + size, 0xFF000000);
|
||||
}
|
||||
}
|
||||
|
||||
public static void DrawIcon(uint iconId, Vector2 position, Vector2 size, bool drawBorder, ImDrawListPtr drawList)
|
||||
{
|
||||
DrawIcon(iconId, position, size, drawBorder, 0xFFFFFFFF, drawList);
|
||||
}
|
||||
|
||||
public static void DrawIcon(uint iconId, Vector2 position, Vector2 size, bool drawBorder, float alpha, ImDrawListPtr drawList)
|
||||
{
|
||||
uint a = (uint)(alpha * 255);
|
||||
uint color = 0xFFFFFF + (a << 24);
|
||||
DrawIcon(iconId, position, size, drawBorder, color, drawList);
|
||||
}
|
||||
|
||||
|
||||
public static void DrawIcon(uint iconId, Vector2 position, Vector2 size, bool drawBorder, uint color, ImDrawListPtr drawList)
|
||||
{
|
||||
IDalamudTextureWrap? texture = TexturesHelper.GetTextureFromIconId(iconId);
|
||||
if (texture == null) { return; }
|
||||
|
||||
drawList.AddImage(texture.Handle, position, position + size, Vector2.Zero, Vector2.One, color);
|
||||
|
||||
if (drawBorder)
|
||||
{
|
||||
drawList.AddRect(position, position + size, 0xFF000000);
|
||||
}
|
||||
}
|
||||
|
||||
public static (Vector2, Vector2) GetTexCoordinates(IDalamudTextureWrap texture, Vector2 size, bool cropIcon = true)
|
||||
{
|
||||
if (texture == null)
|
||||
{
|
||||
return (Vector2.Zero, Vector2.Zero);
|
||||
}
|
||||
|
||||
// Status = 24x32, show from 2,7 until 22,26
|
||||
//show from 0,0 until 24,32 for uncropped status icon
|
||||
|
||||
float uv0x = cropIcon ? 4f : 1f;
|
||||
float uv0y = cropIcon ? 14f : 1f;
|
||||
|
||||
float uv1x = cropIcon ? 4f : 1f;
|
||||
float uv1y = cropIcon ? 12f : 1f;
|
||||
|
||||
Vector2 uv0 = new(uv0x / texture.Width, uv0y / texture.Height);
|
||||
Vector2 uv1 = new(1f - uv1x / texture.Width, 1f - uv1y / texture.Height);
|
||||
|
||||
return (uv0, uv1);
|
||||
}
|
||||
|
||||
public static void DrawIconCooldown(Vector2 position, Vector2 size, float elapsed, float total, ImDrawListPtr drawList)
|
||||
{
|
||||
float completion = elapsed / total;
|
||||
int segments = (int)Math.Ceiling(completion * 4);
|
||||
|
||||
Vector2 center = position + size / 2;
|
||||
|
||||
//Define vertices for top, left, bottom, and right points relative to the center.
|
||||
Vector2[] vertices =
|
||||
[
|
||||
center with {Y = center.Y - size.Y}, // Top
|
||||
center with {X = center.X - size.X}, // Left
|
||||
center with {Y = center.Y + size.Y}, // Bottom
|
||||
center with {X = center.X + size.X} // Right
|
||||
];
|
||||
|
||||
ImGui.PushClipRect(position, position + size, false);
|
||||
for (int i = 0; i < segments; i++)
|
||||
{
|
||||
Vector2 v2 = vertices[i % 4];
|
||||
Vector2 v3 = vertices[(i + 1) % 4];
|
||||
|
||||
|
||||
if (i == segments - 1)
|
||||
{ // If drawing the last segment, adjust the second vertex based on the cooldown.
|
||||
float angle = 2 * MathF.PI * (1 - completion);
|
||||
float cos = MathF.Cos(angle);
|
||||
float sin = MathF.Sin(angle);
|
||||
|
||||
v3 = center + Vector2.Multiply(new Vector2(sin, -cos), size);
|
||||
}
|
||||
|
||||
drawList.AddTriangleFilled(center, v3, v2, 0xCC000000);
|
||||
}
|
||||
ImGui.PopClipRect();
|
||||
}
|
||||
|
||||
public static void DrawOvershield(float shield, Vector2 cursorPos, Vector2 barSize, float height, bool useRatioForHeight, PluginConfigColor color, ImDrawListPtr drawList)
|
||||
{
|
||||
if (shield == 0) { return; }
|
||||
|
||||
float h = useRatioForHeight ? barSize.Y / 100 * height : height;
|
||||
|
||||
DrawGradientFilledRect(cursorPos, new Vector2(Math.Max(1, barSize.X * shield), h), color, drawList);
|
||||
}
|
||||
|
||||
public static void DrawShield(float shield, float hp, Vector2 cursorPos, Vector2 barSize, float height, bool useRatioForHeight, PluginConfigColor color, ImDrawListPtr drawList)
|
||||
{
|
||||
if (shield == 0) { return; }
|
||||
|
||||
// on full hp just draw overshield
|
||||
if (hp == 1)
|
||||
{
|
||||
DrawOvershield(shield, cursorPos, barSize, height, useRatioForHeight, color, drawList);
|
||||
return;
|
||||
}
|
||||
|
||||
// hp portion
|
||||
float h = useRatioForHeight ? barSize.Y / 100 * Math.Min(100, height) : height;
|
||||
float missingHPRatio = 1 - hp;
|
||||
float s = Math.Min(shield, missingHPRatio);
|
||||
Vector2 shieldStartPos = cursorPos + new Vector2(Math.Max(1, barSize.X * hp), 0);
|
||||
DrawGradientFilledRect(shieldStartPos, new Vector2(Math.Max(1, barSize.X * s), barSize.Y), color, drawList);
|
||||
|
||||
// overshield
|
||||
shield -= s;
|
||||
if (shield <= 0) { return; }
|
||||
|
||||
DrawGradientFilledRect(cursorPos, new Vector2(Math.Max(1, barSize.X * shield), h), color, drawList);
|
||||
}
|
||||
|
||||
public static void DrawInWindow(string name, Vector2 pos, Vector2 size, bool needsInput, Action<ImDrawListPtr> drawAction)
|
||||
{
|
||||
const ImGuiWindowFlags windowFlags = ImGuiWindowFlags.NoTitleBar |
|
||||
ImGuiWindowFlags.NoScrollbar |
|
||||
ImGuiWindowFlags.NoBackground |
|
||||
ImGuiWindowFlags.NoMove |
|
||||
ImGuiWindowFlags.NoResize;
|
||||
|
||||
bool inputs = InputsHelper.Instance?.IsProxyEnabled == true ? false : needsInput;
|
||||
|
||||
DrawInWindow(name, pos, size, inputs, false, windowFlags, drawAction);
|
||||
}
|
||||
|
||||
public static void DrawInWindow(
|
||||
string name,
|
||||
Vector2 pos,
|
||||
Vector2 size,
|
||||
bool needsInput,
|
||||
bool needsWindow,
|
||||
ImGuiWindowFlags windowFlags,
|
||||
Action<ImDrawListPtr> drawAction)
|
||||
{
|
||||
|
||||
if (!ClipRectsHelper.Instance.Enabled || ClipRectsHelper.Instance.Mode == WindowClippingMode.Performance)
|
||||
{
|
||||
drawAction(ImGui.GetWindowDrawList());
|
||||
return;
|
||||
}
|
||||
|
||||
windowFlags |= ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoBringToFrontOnFocus;
|
||||
|
||||
if (!needsInput)
|
||||
{
|
||||
windowFlags |= ImGuiWindowFlags.NoInputs;
|
||||
}
|
||||
|
||||
ClipRect? clipRect = ClipRectsHelper.Instance.GetClipRectForArea(pos, size);
|
||||
|
||||
// no clipping needed
|
||||
if (!ClipRectsHelper.Instance.Enabled || !clipRect.HasValue)
|
||||
{
|
||||
ImDrawListPtr drawList = ImGui.GetWindowDrawList();
|
||||
|
||||
if (!needsInput && !needsWindow)
|
||||
{
|
||||
drawAction(drawList);
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui.SetNextWindowPos(pos);
|
||||
ImGui.SetNextWindowSize(size);
|
||||
|
||||
bool begin = ImGui.Begin(name, windowFlags);
|
||||
if (!begin)
|
||||
{
|
||||
ImGui.End();
|
||||
return;
|
||||
}
|
||||
|
||||
drawAction(drawList);
|
||||
|
||||
ImGui.End();
|
||||
}
|
||||
|
||||
// clip around game's window
|
||||
else
|
||||
{
|
||||
// hide instead of clip?
|
||||
if (ClipRectsHelper.Instance.Mode == WindowClippingMode.Hide) { return; }
|
||||
|
||||
ImGuiWindowFlags flags = windowFlags;
|
||||
if (needsInput && clipRect.Value.Contains(ImGui.GetMousePos()))
|
||||
{
|
||||
flags |= ImGuiWindowFlags.NoInputs;
|
||||
}
|
||||
|
||||
ClipRect[] invertedClipRects = ClipRectsHelper.GetInvertedClipRects(clipRect.Value);
|
||||
for (int i = 0; i < invertedClipRects.Length; i++)
|
||||
{
|
||||
ImGui.SetNextWindowPos(pos);
|
||||
ImGui.SetNextWindowSize(size);
|
||||
ImGuiHelpers.ForceNextWindowMainViewport();
|
||||
|
||||
bool begin = ImGui.Begin(name + "_" + i, flags);
|
||||
if (!begin)
|
||||
{
|
||||
ImGui.End();
|
||||
continue;
|
||||
}
|
||||
|
||||
ImGui.PushClipRect(invertedClipRects[i].Min, invertedClipRects[i].Max, false);
|
||||
drawAction(ImGui.GetWindowDrawList());
|
||||
ImGui.PopClipRect();
|
||||
|
||||
ImGui.End();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using FFXIVClientStructs.FFXIV.Client.LayoutEngine;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.String;
|
||||
using FFXIVClientStructs.Interop;
|
||||
using FFXIVClientStructs.STD;
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace HSUI.Helpers
|
||||
{
|
||||
public static class EncryptedStringsHelper
|
||||
{
|
||||
public static unsafe string GetString(string original)
|
||||
{
|
||||
if (!original.StartsWith("_rsv_"))
|
||||
{
|
||||
return original;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
TempLayoutWorld* layoutWorld = (TempLayoutWorld*)LayoutWorld.Instance();
|
||||
StdMap<Utf8String, Pointer<byte>> map = layoutWorld->RsvMap[0];
|
||||
Pointer<byte> demangled = map[new Utf8String(original)];
|
||||
if (demangled.Value != null && Marshal.PtrToStringUTF8((IntPtr)demangled.Value) is { } result)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Plugin.Logger.Error("Error reading rsv map:\n" + e.StackTrace);
|
||||
}
|
||||
|
||||
return original;
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Size = 0x230)]
|
||||
public unsafe struct TempLayoutWorld
|
||||
{
|
||||
[FieldOffset(0x220)] public StdMap<Utf8String, Pointer<byte>>* RsvMap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
using Dalamud.Logging;
|
||||
using Dalamud.Memory;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using StructsFramework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework;
|
||||
|
||||
namespace HSUI.Helpers
|
||||
{
|
||||
public unsafe class ExperienceHelper
|
||||
{
|
||||
#region singleton
|
||||
private static Lazy<ExperienceHelper> _lazyInstance = new Lazy<ExperienceHelper>(() => new ExperienceHelper());
|
||||
private RaptureAtkModule* _raptureAtkModule = null;
|
||||
private const int ExperienceIndex = 2;
|
||||
|
||||
public static ExperienceHelper Instance => _lazyInstance.Value;
|
||||
|
||||
~ExperienceHelper()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lazyInstance = new Lazy<ExperienceHelper>(() => new ExperienceHelper());
|
||||
}
|
||||
#endregion
|
||||
|
||||
public ExperienceHelper()
|
||||
{
|
||||
}
|
||||
|
||||
public AddonExp* GetExpAddon()
|
||||
{
|
||||
return (AddonExp*)Plugin.GameGui.GetAddonByName("_Exp", 1).Address;
|
||||
}
|
||||
|
||||
public uint CurrentExp
|
||||
{
|
||||
get
|
||||
{
|
||||
AddonExp* addon = GetExpAddon();
|
||||
return addon != null ? addon->CurrentExp : 0;
|
||||
}
|
||||
}
|
||||
|
||||
public uint RequiredExp
|
||||
{
|
||||
get
|
||||
{
|
||||
AddonExp* addon = GetExpAddon();
|
||||
return addon != null ? addon->RequiredExp : 0;
|
||||
}
|
||||
}
|
||||
|
||||
public uint RestedExp
|
||||
{
|
||||
get
|
||||
{
|
||||
AddonExp* addon = GetExpAddon();
|
||||
return addon != null ? addon->RestedExp : 0;
|
||||
}
|
||||
}
|
||||
|
||||
public float PercentExp
|
||||
{
|
||||
get
|
||||
{
|
||||
AddonExp* addon = GetExpAddon();
|
||||
return addon != null ? addon->CurrentExpPercent : 0;
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe bool IsMaxLevel()
|
||||
{
|
||||
UIModule* uiModule = StructsFramework.Instance()->GetUIModule();
|
||||
if (uiModule != null)
|
||||
{
|
||||
_raptureAtkModule = uiModule->GetRaptureAtkModule();
|
||||
}
|
||||
|
||||
if (_raptureAtkModule == null || _raptureAtkModule->AtkModule.AtkArrayDataHolder.StringArrayCount <= ExperienceIndex)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var stringArrayData = _raptureAtkModule->AtkModule.AtkArrayDataHolder.StringArrays[ExperienceIndex];
|
||||
var expStringArray = stringArrayData->StringArray[69];
|
||||
var expInfoString = MemoryHelper.ReadSeStringNullTerminated(new IntPtr(expStringArray));
|
||||
return expInfoString.TextValue.Contains("-/-");
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Plugin.Logger.Error("Error when receiving experience information: " + e.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
using Dalamud.Interface.Utility;
|
||||
using HSUI.Config;
|
||||
using HSUI.Interface.GeneralElements;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace HSUI.Helpers
|
||||
{
|
||||
public class FontScope : IDisposable
|
||||
{
|
||||
private readonly IFontHandle? _handle;
|
||||
|
||||
public FontScope(IFontHandle? handle)
|
||||
{
|
||||
_handle = handle;
|
||||
_handle?.Push();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_handle?.Pop();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
public class FontsManager : IDisposable
|
||||
{
|
||||
#region Singleton
|
||||
private FontsManager(string basePath)
|
||||
{
|
||||
DefaultFontsPath = Path.GetDirectoryName(basePath) + "\\Media\\Fonts\\";
|
||||
}
|
||||
|
||||
public static void Initialize(string basePath)
|
||||
{
|
||||
Instance = new FontsManager(basePath);
|
||||
}
|
||||
|
||||
public static FontsManager Instance { get; private set; } = null!;
|
||||
private FontsConfig? _config;
|
||||
|
||||
public void LoadConfig()
|
||||
{
|
||||
if (_config != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_config = ConfigurationManager.Instance.GetConfigObject<FontsConfig>();
|
||||
ConfigurationManager.Instance.ResetEvent += OnConfigReset;
|
||||
}
|
||||
|
||||
private void OnConfigReset(ConfigurationManager sender)
|
||||
{
|
||||
_config = sender.GetConfigObject<FontsConfig>();
|
||||
}
|
||||
|
||||
~FontsManager()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ConfigurationManager.Instance.ResetEvent -= OnConfigReset;
|
||||
Instance = null!;
|
||||
}
|
||||
#endregion
|
||||
|
||||
public readonly string DefaultFontsPath;
|
||||
|
||||
public bool DefaultFontBuilt { get; private set; }
|
||||
public IFontHandle? DefaultFont { get; private set; } = null!;
|
||||
|
||||
private List<IFontHandle> _fonts = new List<IFontHandle>();
|
||||
public IReadOnlyCollection<IFontHandle> Fonts => _fonts.AsReadOnly();
|
||||
|
||||
public FontScope PushDefaultFont()
|
||||
{
|
||||
if (DefaultFontBuilt && DefaultFont != null)
|
||||
{
|
||||
return new FontScope(DefaultFont);
|
||||
}
|
||||
|
||||
return new FontScope(null);
|
||||
}
|
||||
|
||||
public FontScope PushFont(string? fontId)
|
||||
{
|
||||
if (fontId == null || _config == null || !_config.Fonts.ContainsKey(fontId))
|
||||
{
|
||||
return new FontScope(null);
|
||||
}
|
||||
|
||||
var index = _config.Fonts.IndexOfKey(fontId);
|
||||
if (index < 0 || index >= _fonts.Count)
|
||||
{
|
||||
return new FontScope(null);
|
||||
}
|
||||
|
||||
return new FontScope(_fonts[index]);
|
||||
}
|
||||
|
||||
public void ClearFonts()
|
||||
{
|
||||
foreach (IFontHandle font in _fonts)
|
||||
{
|
||||
font.Dispose();
|
||||
}
|
||||
|
||||
_fonts.Clear();
|
||||
}
|
||||
|
||||
public unsafe void BuildFonts()
|
||||
{
|
||||
ClearFonts();
|
||||
DefaultFontBuilt = false;
|
||||
|
||||
FontsConfig config = ConfigurationManager.Instance.GetConfigObject<FontsConfig>();
|
||||
ImGuiIOPtr io = ImGui.GetIO();
|
||||
ushort[]? ranges = GetCharacterRanges(config, io);
|
||||
|
||||
foreach (KeyValuePair<string, FontData> fontData in config.Fonts)
|
||||
{
|
||||
bool isGameFont = config.GameFontMap.ContainsValue(fontData.Value.Name);
|
||||
string path = DefaultFontsPath + fontData.Value.Name + ".ttf";
|
||||
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
path = config.ValidatedFontsPath + fontData.Value.Name + ".ttf";
|
||||
|
||||
if (!File.Exists(path) && !isGameFont)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
IFontHandle font;
|
||||
|
||||
if (isGameFont)
|
||||
{
|
||||
GameFontFamily fontFamily = (GameFontFamily)Enum.Parse(
|
||||
typeof(GameFontFamily),
|
||||
config.GameFontMap.FirstOrDefault(x => x.Value == fontData.Value.Name).Key
|
||||
);
|
||||
GameFontStyle style = new GameFontStyle(fontFamily, fontData.Value.Size);
|
||||
|
||||
font = Plugin.UiBuilder.FontAtlas.NewGameFontHandle(style);
|
||||
}
|
||||
else
|
||||
{
|
||||
font = Plugin.UiBuilder.FontAtlas.NewDelegateFontHandle
|
||||
(
|
||||
e => e.OnPreBuild
|
||||
(
|
||||
tk => tk.AddFontFromFile
|
||||
(
|
||||
path,
|
||||
new SafeFontConfig
|
||||
{
|
||||
SizePx = fontData.Value.Size,
|
||||
GlyphRanges = ranges
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
_fonts.Add(font);
|
||||
|
||||
// save default font
|
||||
if (fontData.Key == FontsConfig.DefaultBigFontKey)
|
||||
{
|
||||
DefaultFont = font;
|
||||
DefaultFontBuilt = true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Logger.Error($"Error loading font from path {path}:\n{ex.Message}");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe ushort[]? GetCharacterRanges(FontsConfig config, ImGuiIOPtr io)
|
||||
{
|
||||
if (!config.SupportChineseCharacters && !config.SupportKoreanCharacters && !config.SupportCyrillicCharacters)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var builder = new ImFontGlyphRangesBuilderPtr(ImGuiNative.ImFontGlyphRangesBuilder());
|
||||
|
||||
if (config.SupportChineseCharacters)
|
||||
{
|
||||
// GetGlyphRangesChineseFull() includes Default + Hiragana, Katakana, Half-Width, Selection of 1946 Ideographs
|
||||
// https://skia.googlesource.com/external/github.com/ocornut/imgui/+/v1.53/extra_fonts/README.txt
|
||||
builder.AddRanges(io.Fonts.GetGlyphRangesChineseFull());
|
||||
}
|
||||
|
||||
if (config.SupportKoreanCharacters)
|
||||
{
|
||||
builder.AddRanges(io.Fonts.GetGlyphRangesKorean());
|
||||
}
|
||||
|
||||
if (config.SupportCyrillicCharacters)
|
||||
{
|
||||
builder.AddRanges(io.Fonts.GetGlyphRangesCyrillic());
|
||||
}
|
||||
|
||||
return builder.BuildRangesToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
Copyright(c) 2021 0ceal0t (https://github.com/0ceal0t/JobBars)
|
||||
Modifications Copyright(c) 2021 HSUI
|
||||
08/29/2021 - Extracted code to get the GCD state of player actions.
|
||||
|
||||
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 FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using System.Collections.Generic;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
|
||||
namespace HSUI.Helpers
|
||||
{
|
||||
internal static class GCDHelper
|
||||
{
|
||||
private static readonly Dictionary<uint, uint> JobActionIDs = new()
|
||||
{
|
||||
[JobIDs.GNB] = 16137, // Keen Edge
|
||||
[JobIDs.WAR] = 31, // Heavy Swing
|
||||
[JobIDs.MRD] = 31, // Heavy Swing
|
||||
[JobIDs.DRK] = 3617, // Hard Slash
|
||||
[JobIDs.PLD] = 9, // Fast Blade
|
||||
[JobIDs.GLA] = 9, // Fast Blade
|
||||
|
||||
[JobIDs.SCH] = 163, // Ruin
|
||||
[JobIDs.AST] = 3596, // Malefic
|
||||
[JobIDs.WHM] = 119, // Stone
|
||||
[JobIDs.CNJ] = 119, // Stone
|
||||
[JobIDs.SGE] = 24283, // Dosis
|
||||
|
||||
[JobIDs.BRD] = 97, // Heavy Shot
|
||||
[JobIDs.ARC] = 97, // Heavy Shot
|
||||
[JobIDs.DNC] = 15989, // Cascade
|
||||
[JobIDs.MCH] = 2866, // Split Shot
|
||||
|
||||
[JobIDs.SMN] = 163, // Ruin
|
||||
[JobIDs.ACN] = 163, // Ruin
|
||||
[JobIDs.RDM] = 7504, // Riposte
|
||||
[JobIDs.BLM] = 142, // Blizzard
|
||||
[JobIDs.THM] = 142, // Blizzard
|
||||
[JobIDs.PCT] = 34650, // Fire in Red
|
||||
|
||||
[JobIDs.SAM] = 7477, // Hakaze
|
||||
[JobIDs.NIN] = 2240, // Spinning Edge
|
||||
[JobIDs.ROG] = 2240, // Spinning Edge
|
||||
[JobIDs.MNK] = 53, // Bootshine
|
||||
[JobIDs.PGL] = 53, // Bootshine
|
||||
[JobIDs.DRG] = 75, // True Thrust
|
||||
[JobIDs.LNC] = 75, // True Thrust
|
||||
[JobIDs.RPR] = 24373, // Slice
|
||||
[JobIDs.VPR] = 34606, // Steel Fangs
|
||||
|
||||
[JobIDs.BLU] = 11385 // Water Cannon
|
||||
};
|
||||
|
||||
public static unsafe bool GetGCDInfo(IPlayerCharacter player, out float timeElapsed, out float timeTotal, ActionType actionType = ActionType.Action)
|
||||
{
|
||||
if (player is null || !JobActionIDs.TryGetValue(player.ClassJob.RowId, out var actionId))
|
||||
{
|
||||
timeElapsed = 0;
|
||||
timeTotal = 0;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
var actionManager = ActionManager.Instance();
|
||||
var adjustedId = actionManager->GetAdjustedActionId(actionId);
|
||||
timeElapsed = actionManager->GetRecastTimeElapsed(actionType, adjustedId);
|
||||
timeTotal = actionManager->GetRecastTime(actionType, adjustedId);
|
||||
|
||||
return timeElapsed > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Logging;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using Newtonsoft.Json;
|
||||
using Lumina.Data.Parsing.Uld;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace HSUI.Helpers
|
||||
{
|
||||
|
||||
public class TitleData
|
||||
{
|
||||
public string Title = "";
|
||||
public bool IsPrefix = false;
|
||||
}
|
||||
|
||||
internal class HonorificHelper
|
||||
{
|
||||
private ICallGateSubscriber<int, string>? _getCharacterTitle;
|
||||
|
||||
#region Singleton
|
||||
private HonorificHelper()
|
||||
{
|
||||
_getCharacterTitle = Plugin.PluginInterface.GetIpcSubscriber<int, string>("Honorific.GetCharacterTitle");
|
||||
}
|
||||
|
||||
public static void Initialize() { Instance = new HonorificHelper(); }
|
||||
|
||||
public static HonorificHelper Instance { get; private set; } = null!;
|
||||
|
||||
~HonorificHelper()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Instance = null!;
|
||||
}
|
||||
#endregion
|
||||
|
||||
public TitleData? GetTitle(IGameObject? actor)
|
||||
{
|
||||
if (_getCharacterTitle == null ||
|
||||
actor == null ||
|
||||
actor.ObjectKind != ObjectKind.Player ||
|
||||
actor is not ICharacter character)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string jsonData = _getCharacterTitle.InvokeFunc(character.ObjectIndex);
|
||||
TitleData? titleData = JsonConvert.DeserializeObject<TitleData>(jsonData ?? string.Empty);
|
||||
return titleData;
|
||||
}
|
||||
catch { }
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Computes HUD layout addon name hashes at runtime using the game's own hash function.
|
||||
* AddonConfigEntry uses CRC32 of "name_a" - UIGlobals.ComputeAddonNameHash does this.
|
||||
* This ensures correct hashes across game patches without hardcoded values.
|
||||
*/
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace HSUI.Helpers
|
||||
{
|
||||
public static class HudLayoutHashHelper
|
||||
{
|
||||
private static readonly Dictionary<string, uint> _cache = new();
|
||||
private static DateTime _lastResolveErrorLog = DateTime.MinValue;
|
||||
private const double ResolveErrorLogIntervalSeconds = 10.0;
|
||||
|
||||
/// <summary>Get AddonNameHash for a layout addon. Addon name is without _a suffix (e.g. "_ParameterWidget").</summary>
|
||||
public static uint GetHash(string addonName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(addonName))
|
||||
return 0;
|
||||
|
||||
if (_cache.TryGetValue(addonName, out var cached))
|
||||
return cached;
|
||||
|
||||
try
|
||||
{
|
||||
uint hash = UIGlobals.ComputeAddonNameHash(addonName);
|
||||
_cache[addonName] = hash;
|
||||
return hash;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
if ((now - _lastResolveErrorLog).TotalSeconds >= ResolveErrorLogIntervalSeconds)
|
||||
{
|
||||
_lastResolveErrorLog = now;
|
||||
Plugin.Logger.Warning($"[HSUI] HudLayoutHashHelper: resolver not ready (e.g. '{addonName}'): {ex.Message}");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Dump all HudLayout addon names and their hashes to the log (for debugging).</summary>
|
||||
public static void DumpHudLayoutAddonsToLog()
|
||||
{
|
||||
try
|
||||
{
|
||||
var span = FFXIVClientStructs.FFXIV.Client.UI.Misc.HudLayoutAddon.GetSpan();
|
||||
Plugin.Logger.Information("[HSUI] HudLayout addon names and hashes (name -> hash):");
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
{
|
||||
ref var addon = ref span[i];
|
||||
if (!addon.AddonName.HasValue) continue;
|
||||
string name = addon.AddonName.ToString() ?? "(null)";
|
||||
if (string.IsNullOrEmpty(name)) continue;
|
||||
uint hash = GetHash(name);
|
||||
Plugin.Logger.Information($" [{i}] {name} -> 0x{hash:X8}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Logger.Error($"[HSUI] HudLayoutHashHelper.DumpHudLayoutAddonsToLog failed: {ex.Message}\n{ex.StackTrace}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
using HSUI.Config;
|
||||
using HSUI.Config.Tree;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
|
||||
namespace HSUI.Helpers
|
||||
{
|
||||
public static class ImGuiHelper
|
||||
{
|
||||
public static void SetTooltip(string? message)
|
||||
{
|
||||
if (message == null) { return; }
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
ImGui.SetTooltip(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static void DrawSeparator(int topSpacing, int bottomSpacing)
|
||||
{
|
||||
DrawSpacing(topSpacing);
|
||||
ImGui.Separator();
|
||||
DrawSpacing(bottomSpacing);
|
||||
}
|
||||
|
||||
public static void DrawSpacing(int spacingSize)
|
||||
{
|
||||
for (int i = 0; i < spacingSize; i++)
|
||||
{
|
||||
ImGui.NewLine();
|
||||
}
|
||||
}
|
||||
|
||||
public static void NewLineAndTab()
|
||||
{
|
||||
ImGui.NewLine();
|
||||
Tab();
|
||||
}
|
||||
|
||||
public static void Tab()
|
||||
{
|
||||
ImGui.Text(" ");
|
||||
ImGui.SameLine();
|
||||
}
|
||||
|
||||
public static Node? DrawExportResetContextMenu(Node node, bool canExport, bool canReset)
|
||||
{
|
||||
Node? nodeToReset = null;
|
||||
|
||||
if (ImGui.BeginPopupContextItem("ResetContextMenu"))
|
||||
{
|
||||
if (canExport && ImGui.Selectable("Export"))
|
||||
{
|
||||
var exportString = node.GetBase64String();
|
||||
ImGui.SetClipboardText(exportString ?? "");
|
||||
}
|
||||
|
||||
if (canReset && ImGui.Selectable("Reset"))
|
||||
{
|
||||
ImGui.CloseCurrentPopup();
|
||||
nodeToReset = node;
|
||||
}
|
||||
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
|
||||
return nodeToReset;
|
||||
}
|
||||
|
||||
public static (bool, bool) DrawConfirmationModal(string title, string message)
|
||||
{
|
||||
return DrawConfirmationModal(title, new string[] { message });
|
||||
}
|
||||
|
||||
public static (bool, bool) DrawConfirmationModal(string title, IEnumerable<string> textLines)
|
||||
{
|
||||
ConfigurationManager.Instance.ShowingModalWindow = true;
|
||||
|
||||
bool didConfirm = false;
|
||||
bool didClose = false;
|
||||
|
||||
ImGui.OpenPopup(title + " ##HSUI");
|
||||
|
||||
Vector2 center = ImGui.GetMainViewport().GetCenter();
|
||||
ImGui.SetNextWindowPos(center, ImGuiCond.Appearing, new Vector2(0.5f, 0.5f));
|
||||
|
||||
bool p_open = true; // i've no idea what this is used for
|
||||
|
||||
if (ImGui.BeginPopupModal(title + " ##HSUI", ref p_open, ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoMove))
|
||||
{
|
||||
float width = 300;
|
||||
float height = Math.Min((ImGui.CalcTextSize(" ").Y + 5) * textLines.Count(), 240);
|
||||
|
||||
ImGui.BeginChild("confirmation_modal_message", new Vector2(width, height), false);
|
||||
foreach (string text in textLines)
|
||||
{
|
||||
ImGui.Text(text);
|
||||
}
|
||||
ImGui.EndChild();
|
||||
|
||||
ImGui.NewLine();
|
||||
|
||||
if (ImGui.Button("OK", new Vector2(width / 2f - 5, 24)))
|
||||
{
|
||||
ImGui.CloseCurrentPopup();
|
||||
didConfirm = true;
|
||||
didClose = true;
|
||||
}
|
||||
|
||||
ImGui.SetItemDefaultFocus();
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Cancel", new Vector2(width / 2f - 5, 24)))
|
||||
{
|
||||
ImGui.CloseCurrentPopup();
|
||||
didClose = true;
|
||||
}
|
||||
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
// close button on nav
|
||||
else
|
||||
{
|
||||
didClose = true;
|
||||
}
|
||||
|
||||
if (didClose)
|
||||
{
|
||||
ConfigurationManager.Instance.ShowingModalWindow = false;
|
||||
}
|
||||
|
||||
return (didConfirm, didClose);
|
||||
}
|
||||
|
||||
public static bool DrawErrorModal(string message)
|
||||
{
|
||||
ConfigurationManager.Instance.ShowingModalWindow = true;
|
||||
|
||||
bool didClose = false;
|
||||
ImGui.OpenPopup("Error ##HSUI");
|
||||
|
||||
Vector2 center = ImGui.GetMainViewport().GetCenter();
|
||||
ImGui.SetNextWindowPos(center, ImGuiCond.Appearing, new Vector2(0.5f, 0.5f));
|
||||
|
||||
bool p_open = true; // i've no idea what this is used for
|
||||
if (ImGui.BeginPopupModal("Error ##HSUI", ref p_open, ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoMove))
|
||||
{
|
||||
ImGui.Text(message);
|
||||
ImGui.NewLine();
|
||||
|
||||
var textSize = ImGui.CalcTextSize(message).X;
|
||||
|
||||
if (ImGui.Button("OK", new Vector2(textSize, 24)))
|
||||
{
|
||||
ImGui.CloseCurrentPopup();
|
||||
didClose = true;
|
||||
}
|
||||
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
// close button on nav
|
||||
else
|
||||
{
|
||||
didClose = true;
|
||||
}
|
||||
|
||||
if (didClose)
|
||||
{
|
||||
ConfigurationManager.Instance.ShowingModalWindow = false;
|
||||
}
|
||||
|
||||
return didClose;
|
||||
}
|
||||
|
||||
public static (bool, bool) DrawInputModal(string title, string message, ref string value)
|
||||
{
|
||||
ConfigurationManager.Instance.ShowingModalWindow = true;
|
||||
|
||||
bool didConfirm = false;
|
||||
bool didClose = false;
|
||||
|
||||
ImGui.OpenPopup(title + " ##HSUI");
|
||||
|
||||
Vector2 center = ImGui.GetMainViewport().GetCenter();
|
||||
ImGui.SetNextWindowPos(center, ImGuiCond.Appearing, new Vector2(0.5f, 0.5f));
|
||||
|
||||
bool p_open = true; // i've no idea what this is used for
|
||||
|
||||
if (ImGui.BeginPopupModal(title + " ##HSUI", ref p_open, ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoMove))
|
||||
{
|
||||
var textSize = ImGui.CalcTextSize(message).X;
|
||||
|
||||
ImGui.Text(message);
|
||||
|
||||
ImGui.PushItemWidth(textSize);
|
||||
ImGui.InputText("", ref value, 64);
|
||||
|
||||
ImGui.NewLine();
|
||||
if (ImGui.Button("OK", new Vector2(textSize / 2f - 5, 24)))
|
||||
{
|
||||
ImGui.CloseCurrentPopup();
|
||||
didConfirm = true;
|
||||
didClose = true;
|
||||
}
|
||||
|
||||
ImGui.SetItemDefaultFocus();
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Cancel", new Vector2(textSize / 2f - 5, 24)))
|
||||
{
|
||||
ImGui.CloseCurrentPopup();
|
||||
didClose = true;
|
||||
}
|
||||
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
// close button on nav
|
||||
else
|
||||
{
|
||||
didClose = true;
|
||||
}
|
||||
|
||||
if (didClose)
|
||||
{
|
||||
ConfigurationManager.Instance.ShowingModalWindow = false;
|
||||
}
|
||||
|
||||
return (didConfirm, didClose);
|
||||
}
|
||||
|
||||
public static string? DrawTextTagsList(string name, ref string searchText)
|
||||
{
|
||||
string? selectedTag = null;
|
||||
|
||||
ImGui.SetNextWindowSize(new(200, 300));
|
||||
|
||||
if (ImGui.BeginPopup(name, ImGuiWindowFlags.NoMove))
|
||||
{
|
||||
if (!ImGui.IsAnyItemActive() && !ImGui.IsAnyItemFocused() && !ImGui.IsMouseClicked(ImGuiMouseButton.Left))
|
||||
{
|
||||
ImGui.SetKeyboardFocusHere(0);
|
||||
}
|
||||
|
||||
// search
|
||||
ImGui.InputText("", ref searchText, 64);
|
||||
|
||||
List<string> keys = new List<string>();
|
||||
keys.AddRange(TextTagsHelper.TextTags.Keys);
|
||||
keys.AddRange(TextTagsHelper.ExpTags.Keys);
|
||||
keys.AddRange(TextTagsHelper.CharaTextTags.Keys);
|
||||
|
||||
foreach (string key in keys)
|
||||
{
|
||||
if (searchText.Length > 0 && !key.Contains(searchText))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// tag
|
||||
if (ImGui.Selectable(key))
|
||||
{
|
||||
selectedTag = key;
|
||||
searchText = "";
|
||||
}
|
||||
|
||||
// help tooltip
|
||||
if (ImGui.IsItemHovered() && Plugin.ObjectTable.LocalPlayer != null)
|
||||
{
|
||||
string formattedText = TextTagsHelper.FormattedText(key, Plugin.ObjectTable.LocalPlayer);
|
||||
|
||||
if (formattedText.Length > 0)
|
||||
{
|
||||
ImGui.SetTooltip("Example: " + formattedText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
|
||||
return selectedTag;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,621 @@
|
||||
/*
|
||||
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);
|
||||
|
||||
return _executeSlotByIdHook != null ? _executeSlotByIdHook.Original(module, hotbarId, slotId) : (byte)0;
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,710 @@
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace HSUI.Helpers
|
||||
{
|
||||
public enum JobRoles
|
||||
{
|
||||
Tank = 0,
|
||||
Healer = 1,
|
||||
DPSMelee = 2,
|
||||
DPSRanged = 3,
|
||||
DPSCaster = 4,
|
||||
Crafter = 5,
|
||||
Gatherer = 6,
|
||||
Unknown
|
||||
}
|
||||
|
||||
public enum PrimaryResourceTypes
|
||||
{
|
||||
MP = 0,
|
||||
CP = 1,
|
||||
GP = 2,
|
||||
None = 3
|
||||
}
|
||||
|
||||
public static class JobsHelper
|
||||
{
|
||||
public static JobRoles RoleForJob(uint jobId)
|
||||
{
|
||||
if (JobRolesMap.TryGetValue(jobId, out var role))
|
||||
{
|
||||
return role;
|
||||
}
|
||||
|
||||
return JobRoles.Unknown;
|
||||
}
|
||||
|
||||
public static bool IsJobARole(uint jobId, JobRoles role)
|
||||
{
|
||||
if (JobRolesMap.TryGetValue(jobId, out var r))
|
||||
{
|
||||
return r == role;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool IsJobTank(uint jobId)
|
||||
{
|
||||
return IsJobARole(jobId, JobRoles.Tank);
|
||||
}
|
||||
|
||||
public static bool IsJobWithCleanse(uint jobId, int level)
|
||||
{
|
||||
var isOnCleanseJob = _cleanseJobs.Contains(jobId);
|
||||
|
||||
if (jobId == JobIDs.BRD && level < 35)
|
||||
{
|
||||
isOnCleanseJob = false;
|
||||
}
|
||||
|
||||
return isOnCleanseJob;
|
||||
}
|
||||
|
||||
private static readonly List<uint> _cleanseJobs = new List<uint>()
|
||||
{
|
||||
JobIDs.CNJ,
|
||||
JobIDs.WHM,
|
||||
JobIDs.SCH,
|
||||
JobIDs.AST,
|
||||
JobIDs.SGE,
|
||||
JobIDs.BRD,
|
||||
JobIDs.BLU
|
||||
};
|
||||
|
||||
public static bool IsJobHealer(uint jobId)
|
||||
{
|
||||
return IsJobARole(jobId, JobRoles.Healer);
|
||||
}
|
||||
|
||||
public static bool IsJobDPS(uint jobId)
|
||||
{
|
||||
if (JobRolesMap.TryGetValue(jobId, out var r))
|
||||
{
|
||||
return r == JobRoles.DPSMelee || r == JobRoles.DPSRanged || r == JobRoles.DPSCaster;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool IsJobDPSMelee(uint jobId)
|
||||
{
|
||||
return IsJobARole(jobId, JobRoles.DPSMelee);
|
||||
}
|
||||
|
||||
public static bool IsJobDPSRanged(uint jobId)
|
||||
{
|
||||
return IsJobARole(jobId, JobRoles.DPSRanged);
|
||||
}
|
||||
|
||||
public static bool IsJobDPSCaster(uint jobId)
|
||||
{
|
||||
return IsJobARole(jobId, JobRoles.DPSCaster);
|
||||
}
|
||||
|
||||
public static bool IsJobCrafter(uint jobId)
|
||||
{
|
||||
return IsJobARole(jobId, JobRoles.Crafter);
|
||||
}
|
||||
|
||||
public static bool IsJobGatherer(uint jobId)
|
||||
{
|
||||
return IsJobARole(jobId, JobRoles.Gatherer);
|
||||
}
|
||||
|
||||
public static bool IsJobWithRaise(uint jobId, uint level)
|
||||
{
|
||||
var isOnRaiseJob = _raiseJobs.Contains(jobId);
|
||||
|
||||
if ((jobId == JobIDs.RDM && level < 64) || level < 12)
|
||||
{
|
||||
isOnRaiseJob = false;
|
||||
}
|
||||
|
||||
return isOnRaiseJob;
|
||||
}
|
||||
|
||||
private static readonly List<uint> _raiseJobs = new List<uint>()
|
||||
{
|
||||
JobIDs.CNJ,
|
||||
JobIDs.WHM,
|
||||
JobIDs.SCH,
|
||||
JobIDs.AST,
|
||||
JobIDs.RDM,
|
||||
JobIDs.SMN,
|
||||
JobIDs.SGE
|
||||
};
|
||||
|
||||
public static uint CurrentPrimaryResource(ICharacter? character)
|
||||
{
|
||||
if (character == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint jobId = character.ClassJob.RowId;
|
||||
|
||||
if (IsJobGatherer(jobId))
|
||||
{
|
||||
return character.CurrentGp;
|
||||
}
|
||||
|
||||
if (IsJobCrafter(jobId))
|
||||
{
|
||||
return character.CurrentCp;
|
||||
}
|
||||
|
||||
return character.CurrentMp;
|
||||
}
|
||||
|
||||
public static uint MaxPrimaryResource(ICharacter? character)
|
||||
{
|
||||
if (character == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint jobId = character.ClassJob.RowId;
|
||||
|
||||
if (IsJobGatherer(jobId))
|
||||
{
|
||||
return character.MaxGp;
|
||||
}
|
||||
|
||||
if (IsJobCrafter(jobId))
|
||||
{
|
||||
return character.MaxCp;
|
||||
}
|
||||
|
||||
return character.MaxMp;
|
||||
}
|
||||
|
||||
public static uint GPResourceRate(ICharacter? character)
|
||||
{
|
||||
if (character == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Preferably I'd want to check the active traits because these traits are locked behind job quests, but no idea how to check traits.
|
||||
|
||||
// Level 83 Trait 239 (MIN), 240 (BTN), 241 (FSH)
|
||||
if (character.Level >= 83)
|
||||
{
|
||||
return 8;
|
||||
}
|
||||
|
||||
// Level 80 Trait 236 (MIN), 237 (BTN), 238 (FSH)
|
||||
if (character.Level >= 80)
|
||||
{
|
||||
return 7;
|
||||
}
|
||||
|
||||
// Level 70 Trait 192 (MIN), 193 (BTN), 194 (FSH)
|
||||
if (character.Level >= 70)
|
||||
{
|
||||
return 6;
|
||||
}
|
||||
|
||||
return 5;
|
||||
}
|
||||
|
||||
public static string TimeTillMaxGP(ICharacter? character)
|
||||
{
|
||||
if (character == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
uint jobId = character.ClassJob.RowId;
|
||||
|
||||
if (!IsJobGatherer(jobId))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
uint gpRate = GPResourceRate(character);
|
||||
|
||||
if (character.CurrentGp == character.MaxGp)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
// Since I'm not using a stopwatch or anything like MPTickHelper here the time will only update every 3 seconds, would be nice if the time ticks down every second.
|
||||
float gpPerSecond = gpRate / 3f;
|
||||
float secondsTillMax = (character.MaxGp - character.CurrentGp) / gpPerSecond;
|
||||
|
||||
return $"{Utils.DurationToString(secondsTillMax)}";
|
||||
}
|
||||
|
||||
public static uint IconIDForJob(uint jobId)
|
||||
{
|
||||
return jobId + 62000;
|
||||
}
|
||||
|
||||
public static uint IconIDForJob(uint jobId, uint style)
|
||||
{
|
||||
if (style < 2)
|
||||
{
|
||||
return IconIDForJob(jobId) + style * 100;
|
||||
}
|
||||
|
||||
ColorizedIconIDs.TryGetValue(jobId, out var iconID);
|
||||
return iconID;
|
||||
}
|
||||
|
||||
public static uint RoleIconIDForJob(uint jobId, bool specificDPSIcons = false)
|
||||
{
|
||||
var role = RoleForJob(jobId);
|
||||
|
||||
switch (role)
|
||||
{
|
||||
case JobRoles.Tank: return 62581;
|
||||
case JobRoles.Healer: return 62582;
|
||||
|
||||
case JobRoles.DPSMelee:
|
||||
case JobRoles.DPSRanged:
|
||||
case JobRoles.DPSCaster:
|
||||
if (specificDPSIcons && SpecificDPSIcons.TryGetValue(jobId, out var iconId))
|
||||
{
|
||||
return iconId;
|
||||
}
|
||||
else
|
||||
{
|
||||
return 62583;
|
||||
}
|
||||
|
||||
case JobRoles.Gatherer:
|
||||
case JobRoles.Crafter:
|
||||
return IconIDForJob(jobId);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static uint RoleIconIDForBattleCompanion => 62043;
|
||||
|
||||
public static Dictionary<uint, JobRoles> JobRolesMap = new Dictionary<uint, JobRoles>()
|
||||
{
|
||||
// tanks
|
||||
[JobIDs.GLA] = JobRoles.Tank,
|
||||
[JobIDs.MRD] = JobRoles.Tank,
|
||||
[JobIDs.PLD] = JobRoles.Tank,
|
||||
[JobIDs.WAR] = JobRoles.Tank,
|
||||
[JobIDs.DRK] = JobRoles.Tank,
|
||||
[JobIDs.GNB] = JobRoles.Tank,
|
||||
|
||||
// healers
|
||||
[JobIDs.CNJ] = JobRoles.Healer,
|
||||
[JobIDs.WHM] = JobRoles.Healer,
|
||||
[JobIDs.SCH] = JobRoles.Healer,
|
||||
[JobIDs.AST] = JobRoles.Healer,
|
||||
[JobIDs.SGE] = JobRoles.Healer,
|
||||
|
||||
// melee dps
|
||||
[JobIDs.PGL] = JobRoles.DPSMelee,
|
||||
[JobIDs.LNC] = JobRoles.DPSMelee,
|
||||
[JobIDs.ROG] = JobRoles.DPSMelee,
|
||||
[JobIDs.MNK] = JobRoles.DPSMelee,
|
||||
[JobIDs.DRG] = JobRoles.DPSMelee,
|
||||
[JobIDs.NIN] = JobRoles.DPSMelee,
|
||||
[JobIDs.SAM] = JobRoles.DPSMelee,
|
||||
[JobIDs.RPR] = JobRoles.DPSMelee,
|
||||
[JobIDs.VPR] = JobRoles.DPSMelee,
|
||||
|
||||
// ranged phys dps
|
||||
[JobIDs.ARC] = JobRoles.DPSRanged,
|
||||
[JobIDs.BRD] = JobRoles.DPSRanged,
|
||||
[JobIDs.MCH] = JobRoles.DPSRanged,
|
||||
[JobIDs.DNC] = JobRoles.DPSRanged,
|
||||
|
||||
// ranged magic dps
|
||||
[JobIDs.THM] = JobRoles.DPSCaster,
|
||||
[JobIDs.ACN] = JobRoles.DPSCaster,
|
||||
[JobIDs.BLM] = JobRoles.DPSCaster,
|
||||
[JobIDs.SMN] = JobRoles.DPSCaster,
|
||||
[JobIDs.RDM] = JobRoles.DPSCaster,
|
||||
[JobIDs.BLU] = JobRoles.DPSCaster,
|
||||
[JobIDs.PCT] = JobRoles.DPSCaster,
|
||||
|
||||
// crafters
|
||||
[JobIDs.CRP] = JobRoles.Crafter,
|
||||
[JobIDs.BSM] = JobRoles.Crafter,
|
||||
[JobIDs.ARM] = JobRoles.Crafter,
|
||||
[JobIDs.GSM] = JobRoles.Crafter,
|
||||
[JobIDs.LTW] = JobRoles.Crafter,
|
||||
[JobIDs.WVR] = JobRoles.Crafter,
|
||||
[JobIDs.ALC] = JobRoles.Crafter,
|
||||
[JobIDs.CUL] = JobRoles.Crafter,
|
||||
|
||||
// gatherers
|
||||
[JobIDs.MIN] = JobRoles.Gatherer,
|
||||
[JobIDs.BOT] = JobRoles.Gatherer,
|
||||
[JobIDs.FSH] = JobRoles.Gatherer,
|
||||
};
|
||||
|
||||
public static Dictionary<JobRoles, List<uint>> JobsByRole = new Dictionary<JobRoles, List<uint>>()
|
||||
{
|
||||
// tanks
|
||||
[JobRoles.Tank] = new List<uint>() {
|
||||
JobIDs.GLA,
|
||||
JobIDs.MRD,
|
||||
JobIDs.PLD,
|
||||
JobIDs.WAR,
|
||||
JobIDs.DRK,
|
||||
JobIDs.GNB,
|
||||
},
|
||||
|
||||
// healers
|
||||
[JobRoles.Healer] = new List<uint>()
|
||||
{
|
||||
JobIDs.CNJ,
|
||||
JobIDs.WHM,
|
||||
JobIDs.SCH,
|
||||
JobIDs.AST,
|
||||
JobIDs.SGE
|
||||
},
|
||||
|
||||
// melee dps
|
||||
[JobRoles.DPSMelee] = new List<uint>() {
|
||||
JobIDs.PGL,
|
||||
JobIDs.LNC,
|
||||
JobIDs.ROG,
|
||||
JobIDs.MNK,
|
||||
JobIDs.DRG,
|
||||
JobIDs.NIN,
|
||||
JobIDs.SAM,
|
||||
JobIDs.RPR,
|
||||
JobIDs.VPR
|
||||
},
|
||||
|
||||
// ranged phys dps
|
||||
[JobRoles.DPSRanged] = new List<uint>()
|
||||
{
|
||||
JobIDs.ARC,
|
||||
JobIDs.BRD,
|
||||
JobIDs.MCH,
|
||||
JobIDs.DNC,
|
||||
},
|
||||
|
||||
// ranged magic dps
|
||||
[JobRoles.DPSCaster] = new List<uint>()
|
||||
{
|
||||
JobIDs.THM,
|
||||
JobIDs.ACN,
|
||||
JobIDs.BLM,
|
||||
JobIDs.SMN,
|
||||
JobIDs.RDM,
|
||||
JobIDs.BLU,
|
||||
JobIDs.PCT
|
||||
},
|
||||
|
||||
// crafters
|
||||
[JobRoles.Crafter] = new List<uint>()
|
||||
{
|
||||
JobIDs.CRP,
|
||||
JobIDs.BSM,
|
||||
JobIDs.ARM,
|
||||
JobIDs.GSM,
|
||||
JobIDs.LTW,
|
||||
JobIDs.WVR,
|
||||
JobIDs.ALC,
|
||||
JobIDs.CUL,
|
||||
},
|
||||
|
||||
// gatherers
|
||||
[JobRoles.Gatherer] = new List<uint>()
|
||||
{
|
||||
JobIDs.MIN,
|
||||
JobIDs.BOT,
|
||||
JobIDs.FSH,
|
||||
},
|
||||
|
||||
// unknown
|
||||
[JobRoles.Unknown] = new List<uint>()
|
||||
};
|
||||
|
||||
public static Dictionary<uint, string> JobNames = new Dictionary<uint, string>()
|
||||
{
|
||||
// tanks
|
||||
[JobIDs.GLA] = "GLA",
|
||||
[JobIDs.MRD] = "MRD",
|
||||
[JobIDs.PLD] = "PLD",
|
||||
[JobIDs.WAR] = "WAR",
|
||||
[JobIDs.DRK] = "DRK",
|
||||
[JobIDs.GNB] = "GNB",
|
||||
|
||||
// melee dps
|
||||
[JobIDs.PGL] = "PGL",
|
||||
[JobIDs.LNC] = "LNC",
|
||||
[JobIDs.ROG] = "ROG",
|
||||
[JobIDs.MNK] = "MNK",
|
||||
[JobIDs.DRG] = "DRG",
|
||||
[JobIDs.NIN] = "NIN",
|
||||
[JobIDs.SAM] = "SAM",
|
||||
[JobIDs.RPR] = "RPR",
|
||||
[JobIDs.VPR] = "VPR",
|
||||
|
||||
// ranged phys dps
|
||||
[JobIDs.ARC] = "ARC",
|
||||
[JobIDs.BRD] = "BRD",
|
||||
[JobIDs.MCH] = "MCH",
|
||||
[JobIDs.DNC] = "DNC",
|
||||
|
||||
// ranged magic dps
|
||||
[JobIDs.THM] = "THM",
|
||||
[JobIDs.ACN] = "ACN",
|
||||
[JobIDs.BLM] = "BLM",
|
||||
[JobIDs.SMN] = "SMN",
|
||||
[JobIDs.RDM] = "RDM",
|
||||
[JobIDs.BLU] = "BLU",
|
||||
[JobIDs.PCT] = "PCT",
|
||||
|
||||
// healers
|
||||
[JobIDs.CNJ] = "CNJ",
|
||||
[JobIDs.WHM] = "WHM",
|
||||
[JobIDs.SCH] = "SCH",
|
||||
[JobIDs.SGE] = "SGE",
|
||||
[JobIDs.AST] = "AST",
|
||||
|
||||
// crafters
|
||||
[JobIDs.CRP] = "CRP",
|
||||
[JobIDs.BSM] = "BSM",
|
||||
[JobIDs.ARM] = "ARM",
|
||||
[JobIDs.GSM] = "GSM",
|
||||
[JobIDs.LTW] = "LTW",
|
||||
[JobIDs.WVR] = "WVR",
|
||||
[JobIDs.ALC] = "ALC",
|
||||
[JobIDs.CUL] = "CUL",
|
||||
|
||||
// gatherers
|
||||
[JobIDs.MIN] = "MIN",
|
||||
[JobIDs.BOT] = "BOT",
|
||||
[JobIDs.FSH] = "FSH",
|
||||
};
|
||||
|
||||
public static Dictionary<uint, string> JobFullNames = new Dictionary<uint, string>()
|
||||
{
|
||||
// tanks
|
||||
[JobIDs.GLA] = "Gladiator",
|
||||
[JobIDs.MRD] = "Marauder",
|
||||
[JobIDs.PLD] = "Paladin",
|
||||
[JobIDs.WAR] = "Warrior",
|
||||
[JobIDs.DRK] = "Dark Knight",
|
||||
[JobIDs.GNB] = "Gunbreaker",
|
||||
|
||||
// melee dps
|
||||
[JobIDs.PGL] = "Pugilist",
|
||||
[JobIDs.LNC] = "Lancer",
|
||||
[JobIDs.ROG] = "Rogue",
|
||||
[JobIDs.MNK] = "Monk",
|
||||
[JobIDs.DRG] = "Dragoon",
|
||||
[JobIDs.NIN] = "Ninja",
|
||||
[JobIDs.SAM] = "Samurai",
|
||||
[JobIDs.RPR] = "Reaper",
|
||||
[JobIDs.VPR] = "Viper",
|
||||
|
||||
// ranged phys dps
|
||||
[JobIDs.ARC] = "Archer",
|
||||
[JobIDs.BRD] = "Bard",
|
||||
[JobIDs.MCH] = "Machinist",
|
||||
[JobIDs.DNC] = "Dancer",
|
||||
|
||||
// ranged magic dps
|
||||
[JobIDs.THM] = "Thaumaturge",
|
||||
[JobIDs.ACN] = "Arcanist",
|
||||
[JobIDs.BLM] = "Black Mage",
|
||||
[JobIDs.SMN] = "Summoner",
|
||||
[JobIDs.RDM] = "Red Mage",
|
||||
[JobIDs.BLU] = "Blue Mage",
|
||||
[JobIDs.PCT] = "Pictomancer",
|
||||
|
||||
// healers
|
||||
[JobIDs.CNJ] = "Conjurer",
|
||||
[JobIDs.WHM] = "White Mage",
|
||||
[JobIDs.SCH] = "Scholar",
|
||||
[JobIDs.SGE] = "Sage",
|
||||
[JobIDs.AST] = "Astrologian",
|
||||
|
||||
// crafters
|
||||
[JobIDs.CRP] = "Carpenter",
|
||||
[JobIDs.BSM] = "Blacksmith",
|
||||
[JobIDs.ARM] = "Armorer",
|
||||
[JobIDs.GSM] = "Goldsmith",
|
||||
[JobIDs.LTW] = "Leatherworker",
|
||||
[JobIDs.WVR] = "Weaver",
|
||||
[JobIDs.ALC] = "Alchemist",
|
||||
[JobIDs.CUL] = "Culinarian",
|
||||
|
||||
// gatherers
|
||||
[JobIDs.MIN] = "Miner",
|
||||
[JobIDs.BOT] = "Botanist",
|
||||
[JobIDs.FSH] = "Fisher",
|
||||
};
|
||||
|
||||
public static Dictionary<JobRoles, string> RoleNames = new Dictionary<JobRoles, string>()
|
||||
{
|
||||
[JobRoles.Tank] = "Tank",
|
||||
[JobRoles.Healer] = "Healer",
|
||||
[JobRoles.DPSMelee] = "Melee",
|
||||
[JobRoles.DPSRanged] = "Ranged",
|
||||
[JobRoles.DPSCaster] = "Caster",
|
||||
[JobRoles.Crafter] = "Crafter",
|
||||
[JobRoles.Gatherer] = "Gatherer",
|
||||
[JobRoles.Unknown] = "Unknown"
|
||||
};
|
||||
|
||||
public static Dictionary<uint, uint> SpecificDPSIcons = new Dictionary<uint, uint>()
|
||||
{
|
||||
// melee dps
|
||||
[JobIDs.PGL] = 62584,
|
||||
[JobIDs.LNC] = 62584,
|
||||
[JobIDs.ROG] = 62584,
|
||||
[JobIDs.MNK] = 62584,
|
||||
[JobIDs.DRG] = 62584,
|
||||
[JobIDs.NIN] = 62584,
|
||||
[JobIDs.SAM] = 62584,
|
||||
[JobIDs.RPR] = 62584,
|
||||
[JobIDs.VPR] = 62584,
|
||||
|
||||
// ranged phys dps
|
||||
[JobIDs.ARC] = 62586,
|
||||
[JobIDs.BRD] = 62586,
|
||||
[JobIDs.MCH] = 62586,
|
||||
[JobIDs.DNC] = 62586,
|
||||
|
||||
// ranged magic dps
|
||||
[JobIDs.THM] = 62587,
|
||||
[JobIDs.ACN] = 62587,
|
||||
[JobIDs.BLM] = 62587,
|
||||
[JobIDs.SMN] = 62587,
|
||||
[JobIDs.RDM] = 62587,
|
||||
[JobIDs.BLU] = 62587,
|
||||
[JobIDs.PCT] = 62587
|
||||
};
|
||||
|
||||
public static Dictionary<uint, uint> ColorizedIconIDs = new Dictionary<uint, uint>()
|
||||
{
|
||||
// tanks
|
||||
[JobIDs.GLA] = 94022,
|
||||
[JobIDs.MRD] = 94024,
|
||||
[JobIDs.PLD] = 94079,
|
||||
[JobIDs.WAR] = 94081,
|
||||
[JobIDs.DRK] = 94123,
|
||||
[JobIDs.GNB] = 94130,
|
||||
|
||||
// melee dps
|
||||
[JobIDs.PGL] = 92523,
|
||||
[JobIDs.LNC] = 92525,
|
||||
[JobIDs.ROG] = 92621,
|
||||
[JobIDs.MNK] = 92580,
|
||||
[JobIDs.DRG] = 92582,
|
||||
[JobIDs.NIN] = 92622,
|
||||
[JobIDs.SAM] = 92627,
|
||||
[JobIDs.RPR] = 92632,
|
||||
[JobIDs.VPR] = 92685,
|
||||
|
||||
// ranged phys dps
|
||||
[JobIDs.ARC] = 92526,
|
||||
[JobIDs.BRD] = 92583,
|
||||
[JobIDs.MCH] = 92625,
|
||||
[JobIDs.DNC] = 92631,
|
||||
|
||||
// ranged magic dps
|
||||
[JobIDs.THM] = 92529,
|
||||
[JobIDs.ACN] = 92530,
|
||||
[JobIDs.BLM] = 92585,
|
||||
[JobIDs.SMN] = 92586,
|
||||
[JobIDs.RDM] = 92628,
|
||||
[JobIDs.BLU] = 92629,
|
||||
[JobIDs.PCT] = 92686,
|
||||
|
||||
// healers
|
||||
[JobIDs.CNJ] = 94528,
|
||||
[JobIDs.WHM] = 94584,
|
||||
[JobIDs.SCH] = 94587,
|
||||
[JobIDs.SGE] = 94633,
|
||||
[JobIDs.AST] = 94624,
|
||||
|
||||
// crafters
|
||||
[JobIDs.CRP] = 91031,
|
||||
[JobIDs.BSM] = 91032,
|
||||
[JobIDs.ARM] = 91033,
|
||||
[JobIDs.GSM] = 91034,
|
||||
[JobIDs.LTW] = 91034,
|
||||
[JobIDs.WVR] = 91036,
|
||||
[JobIDs.ALC] = 91037,
|
||||
[JobIDs.CUL] = 91038,
|
||||
|
||||
// gatherers
|
||||
[JobIDs.MIN] = 91039,
|
||||
[JobIDs.BOT] = 91039,
|
||||
[JobIDs.FSH] = 91041,
|
||||
};
|
||||
|
||||
public static Dictionary<JobRoles, PrimaryResourceTypes> PrimaryResourceTypesByRole = new Dictionary<JobRoles, PrimaryResourceTypes>()
|
||||
{
|
||||
[JobRoles.Tank] = PrimaryResourceTypes.MP,
|
||||
[JobRoles.Healer] = PrimaryResourceTypes.MP,
|
||||
[JobRoles.DPSMelee] = PrimaryResourceTypes.MP,
|
||||
[JobRoles.DPSRanged] = PrimaryResourceTypes.MP,
|
||||
[JobRoles.DPSCaster] = PrimaryResourceTypes.MP,
|
||||
[JobRoles.Crafter] = PrimaryResourceTypes.CP,
|
||||
[JobRoles.Gatherer] = PrimaryResourceTypes.GP,
|
||||
[JobRoles.Unknown] = PrimaryResourceTypes.MP
|
||||
};
|
||||
}
|
||||
|
||||
public static class JobIDs
|
||||
{
|
||||
public const uint GLA = 1;
|
||||
public const uint MRD = 3;
|
||||
public const uint PLD = 19;
|
||||
public const uint WAR = 21;
|
||||
public const uint DRK = 32;
|
||||
public const uint GNB = 37;
|
||||
|
||||
public const uint CNJ = 6;
|
||||
public const uint WHM = 24;
|
||||
public const uint SCH = 28;
|
||||
public const uint AST = 33;
|
||||
public const uint SGE = 40;
|
||||
|
||||
public const uint PGL = 2;
|
||||
public const uint LNC = 4;
|
||||
public const uint ROG = 29;
|
||||
public const uint MNK = 20;
|
||||
public const uint DRG = 22;
|
||||
public const uint NIN = 30;
|
||||
public const uint SAM = 34;
|
||||
public const uint RPR = 39;
|
||||
public const uint VPR = 41;
|
||||
|
||||
public const uint ARC = 5;
|
||||
public const uint BRD = 23;
|
||||
public const uint MCH = 31;
|
||||
public const uint DNC = 38;
|
||||
|
||||
public const uint THM = 7;
|
||||
public const uint ACN = 26;
|
||||
public const uint BLM = 25;
|
||||
public const uint SMN = 27;
|
||||
public const uint RDM = 35;
|
||||
public const uint BLU = 36;
|
||||
public const uint PCT = 42;
|
||||
|
||||
public const uint CRP = 8;
|
||||
public const uint BSM = 9;
|
||||
public const uint ARM = 10;
|
||||
public const uint GSM = 11;
|
||||
public const uint LTW = 12;
|
||||
public const uint WVR = 13;
|
||||
public const uint ALC = 14;
|
||||
public const uint CUL = 15;
|
||||
|
||||
public const uint MIN = 16;
|
||||
public const uint BOT = 17;
|
||||
public const uint FSH = 18;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Interface.Textures.TextureWraps;
|
||||
using HSUI.Enums;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using Lumina.Excel;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Companion = Lumina.Excel.Sheets.Companion;
|
||||
|
||||
namespace HSUI.Helpers
|
||||
{
|
||||
public class LastUsedCast
|
||||
{
|
||||
private object? _lastUsedAction;
|
||||
|
||||
public readonly bool Interruptible;
|
||||
public readonly ActionType ActionType;
|
||||
public readonly uint CastId;
|
||||
private uint? _iconId;
|
||||
|
||||
public string ActionText { get; private set; } = "";
|
||||
public DamageType DamageType { get; private set; } = DamageType.Unknown;
|
||||
|
||||
public LastUsedCast(uint castId, ActionType actionType, bool interruptible)
|
||||
{
|
||||
CastId = castId;
|
||||
ActionType = actionType;
|
||||
Interruptible = interruptible;
|
||||
|
||||
SetCastProperties();
|
||||
}
|
||||
|
||||
private void SetCastProperties()
|
||||
{
|
||||
IGameObject? target = Plugin.TargetManager.SoftTarget ?? Plugin.TargetManager.Target;
|
||||
ObjectKind? targetKind = target?.ObjectKind;
|
||||
|
||||
switch (targetKind)
|
||||
{
|
||||
case null:
|
||||
break;
|
||||
|
||||
case ObjectKind.Aetheryte:
|
||||
ActionText = "Attuning...";
|
||||
_iconId = 112;
|
||||
|
||||
return;
|
||||
|
||||
case ObjectKind.EventObj:
|
||||
case ObjectKind.EventNpc:
|
||||
ActionText = "Interacting...";
|
||||
_iconId = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_lastUsedAction = null;
|
||||
if (CastId == 1 && ActionType != ActionType.Mount)
|
||||
{
|
||||
ActionText = "Interacting...";
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ActionText = "Casting";
|
||||
_iconId = null;
|
||||
|
||||
switch (ActionType)
|
||||
{
|
||||
case ActionType.PetAction:
|
||||
case ActionType.Action:
|
||||
case ActionType.BgcArmyAction:
|
||||
case ActionType.PvPAction:
|
||||
case ActionType.CraftAction:
|
||||
case ActionType.EventAction:
|
||||
Action? action = Plugin.DataManager.GetExcelSheet<Action>()?.GetRow(CastId);
|
||||
ActionText = action?.Name.ToString() ?? "";
|
||||
DamageType = GetDamageType(action);
|
||||
_lastUsedAction = action;
|
||||
|
||||
break;
|
||||
|
||||
case ActionType.Mount:
|
||||
Mount? mount = Plugin.DataManager.GetExcelSheet<Mount>()?.GetRow(CastId);
|
||||
ActionText = mount?.Singular.ToString() ?? "";
|
||||
DamageType = DamageType.Unknown;
|
||||
_lastUsedAction = mount;
|
||||
break;
|
||||
|
||||
case ActionType.EventItem:
|
||||
case ActionType.Item:
|
||||
Item? item = Plugin.DataManager.GetExcelSheet<Item>()?.GetRow(CastId);
|
||||
ActionText = item?.Name.ToString() ?? "Using item...";
|
||||
DamageType = DamageType.Unknown;
|
||||
_lastUsedAction = item;
|
||||
break;
|
||||
|
||||
case ActionType.Companion:
|
||||
Companion? companion = Plugin.DataManager.GetExcelSheet<Companion>()?.GetRow(CastId);
|
||||
ActionText = companion?.Singular.ToString() ?? "";
|
||||
DamageType = DamageType.Unknown;
|
||||
_lastUsedAction = companion;
|
||||
break;
|
||||
|
||||
default:
|
||||
_lastUsedAction = null;
|
||||
ActionText = "Casting...";
|
||||
DamageType = DamageType.Unknown;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static DamageType GetDamageType(Action? action)
|
||||
{
|
||||
if (!action.HasValue)
|
||||
{
|
||||
return DamageType.Unknown;
|
||||
}
|
||||
|
||||
DamageType damageType = (DamageType)action.Value.AttackType.RowId;
|
||||
|
||||
if (damageType != DamageType.Magic && damageType != DamageType.Darkness && damageType != DamageType.Unknown)
|
||||
{
|
||||
damageType = DamageType.Physical;
|
||||
}
|
||||
|
||||
return damageType;
|
||||
}
|
||||
|
||||
public IDalamudTextureWrap? GetIconTexture()
|
||||
{
|
||||
if (_iconId.HasValue)
|
||||
{
|
||||
return TexturesHelper.GetTexture<Action>(_iconId.Value);
|
||||
}
|
||||
else if (_lastUsedAction is Action action)
|
||||
{
|
||||
return TexturesHelper.GetTextureFromIconId(action.Icon);
|
||||
}
|
||||
else if (_lastUsedAction is Mount mount)
|
||||
{
|
||||
return TexturesHelper.GetTextureFromIconId(mount.Icon);
|
||||
}
|
||||
else if (_lastUsedAction is Item item)
|
||||
{
|
||||
return TexturesHelper.GetTextureFromIconId(item.Icon);
|
||||
}
|
||||
else if (_lastUsedAction is Companion companion)
|
||||
{
|
||||
return TexturesHelper.GetTextureFromIconId(companion.Icon);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
namespace HSUI.Helpers
|
||||
{
|
||||
public struct LayoutInfo
|
||||
{
|
||||
public readonly uint TotalRowCount;
|
||||
public readonly uint TotalColCount;
|
||||
public readonly uint RealRowCount;
|
||||
public readonly uint RealColCount;
|
||||
public readonly Vector2 ContentSize;
|
||||
|
||||
public LayoutInfo(uint totalRowCount, uint totalColCount, uint realRowCount, uint realColCount, Vector2 contentSize)
|
||||
{
|
||||
TotalRowCount = totalRowCount;
|
||||
TotalColCount = totalColCount;
|
||||
RealRowCount = realRowCount;
|
||||
RealColCount = realColCount;
|
||||
ContentSize = contentSize;
|
||||
}
|
||||
}
|
||||
|
||||
public static class LayoutHelper
|
||||
{
|
||||
// Calculates rows and columns. Used for status effect lists and party frames.
|
||||
public static LayoutInfo CalculateLayout(
|
||||
Vector2 maxSize,
|
||||
Vector2 itemSize,
|
||||
uint count,
|
||||
Vector2 padding,
|
||||
bool fillRowsFirst
|
||||
)
|
||||
{
|
||||
uint rowCount = 1;
|
||||
uint colCount = 1;
|
||||
uint realRowCount = 1;
|
||||
uint realColCount = 1;
|
||||
|
||||
if (maxSize.X < itemSize.X)
|
||||
{
|
||||
colCount = count;
|
||||
realColCount = count;
|
||||
}
|
||||
else if (maxSize.Y < itemSize.Y)
|
||||
{
|
||||
rowCount = count;
|
||||
realRowCount = count;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (fillRowsFirst)
|
||||
{
|
||||
float remainingWidth = maxSize.X;
|
||||
colCount = 0;
|
||||
while (remainingWidth > 0)
|
||||
{
|
||||
remainingWidth -= (itemSize.X + padding.X);
|
||||
colCount++;
|
||||
}
|
||||
|
||||
if (itemSize.X * colCount + padding.X * (colCount - 1) > maxSize.X)
|
||||
{
|
||||
colCount = Math.Max(1, colCount - 1);
|
||||
}
|
||||
|
||||
rowCount = (uint)Math.Ceiling((double)count / colCount);
|
||||
|
||||
int remaining = (int)(count - colCount);
|
||||
while (remaining > 0)
|
||||
{
|
||||
realRowCount++;
|
||||
remaining -= (int)colCount;
|
||||
}
|
||||
|
||||
realColCount = Math.Min(count, colCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
float remainingHeight = maxSize.Y;
|
||||
rowCount = 0;
|
||||
while (remainingHeight > 0)
|
||||
{
|
||||
remainingHeight -= (itemSize.Y + padding.Y);
|
||||
rowCount++;
|
||||
}
|
||||
|
||||
if (itemSize.Y * rowCount + padding.Y * (rowCount - 1) > maxSize.Y)
|
||||
{
|
||||
rowCount = Math.Max(1, rowCount - 1);
|
||||
}
|
||||
|
||||
colCount = (uint)Math.Ceiling((double)count / rowCount);
|
||||
|
||||
int remaining = (int)(count - rowCount);
|
||||
while (remaining > 0)
|
||||
{
|
||||
realColCount++;
|
||||
remaining -= (int)rowCount;
|
||||
}
|
||||
|
||||
realRowCount = Math.Min(count, rowCount);
|
||||
}
|
||||
}
|
||||
|
||||
Vector2 contentSize = new Vector2(
|
||||
realColCount * itemSize.X + (realColCount - 1) * padding.X,
|
||||
realRowCount * itemSize.Y + (realRowCount - 1) * padding.Y
|
||||
);
|
||||
|
||||
return new LayoutInfo(rowCount, colCount, realRowCount, realColCount, contentSize);
|
||||
}
|
||||
|
||||
private static List<GrowthDirections> DirectionOptionsValues = new List<GrowthDirections>()
|
||||
{
|
||||
GrowthDirections.Right | GrowthDirections.Down,
|
||||
GrowthDirections.Right | GrowthDirections.Up,
|
||||
GrowthDirections.Left | GrowthDirections.Down,
|
||||
GrowthDirections.Left | GrowthDirections.Up,
|
||||
GrowthDirections.Centered | GrowthDirections.Up,
|
||||
GrowthDirections.Centered | GrowthDirections.Down,
|
||||
GrowthDirections.Centered | GrowthDirections.Left,
|
||||
GrowthDirections.Centered | GrowthDirections.Right
|
||||
};
|
||||
public static GrowthDirections GrowthDirectionsFromIndex(int index)
|
||||
{
|
||||
if (index > 0 && index < DirectionOptionsValues.Count)
|
||||
{
|
||||
return DirectionOptionsValues[index];
|
||||
}
|
||||
|
||||
return DirectionOptionsValues[0];
|
||||
}
|
||||
|
||||
public static int IndexFromGrowthDirections(GrowthDirections directions)
|
||||
{
|
||||
int index = DirectionOptionsValues.FindIndex(d => d == directions);
|
||||
|
||||
return index > 0 ? index : 0;
|
||||
}
|
||||
|
||||
public static bool GetFillsRowsFirst(bool fallback, GrowthDirections directions)
|
||||
{
|
||||
if ((directions & GrowthDirections.Centered) != 0)
|
||||
{
|
||||
if ((directions & GrowthDirections.Up) != 0 || (directions & GrowthDirections.Down) != 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if ((directions & GrowthDirections.Left) != 0 || (directions & GrowthDirections.Right) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
public static void CalculateAxisDirections(
|
||||
GrowthDirections growthDirections,
|
||||
int row,
|
||||
int col,
|
||||
uint elementCount,
|
||||
Vector2 size,
|
||||
Vector2 iconSize,
|
||||
Vector2 iconPadding,
|
||||
out Vector2 direction,
|
||||
out Vector2 offset)
|
||||
{
|
||||
if ((growthDirections & GrowthDirections.Centered) != 0)
|
||||
{
|
||||
if ((growthDirections & GrowthDirections.Up) != 0 || (growthDirections & GrowthDirections.Down) != 0)
|
||||
{
|
||||
int elementsPerRow = (int)(size.X / (iconSize.X + iconPadding.X));
|
||||
long elementsInRow = Math.Min(elementsPerRow, elementCount - (elementsPerRow * row));
|
||||
|
||||
direction.X = 1;
|
||||
direction.Y = (growthDirections & GrowthDirections.Down) != 0 ? 1 : -1;
|
||||
offset.X = -(iconSize.X + iconPadding.X) * elementsInRow / 2f;
|
||||
offset.Y = direction.Y == 1 ? 0 : -iconSize.Y;
|
||||
}
|
||||
|
||||
else// if ((growthDirections & GrowthDirections.Left) != 0 || (growthDirections & GrowthDirections.Right) != 0)
|
||||
{
|
||||
int elementsPerCol = (int)(size.Y / (iconSize.Y + iconPadding.Y));
|
||||
long elementsInCol = Math.Min(elementsPerCol, elementCount - (elementsPerCol * col));
|
||||
|
||||
direction.X = (growthDirections & GrowthDirections.Left) != 0 ? -1 : 1;
|
||||
direction.Y = 1;
|
||||
offset.X = direction.X == 1 ? 0 : -iconSize.X;
|
||||
offset.Y = -(iconSize.Y + iconPadding.Y) * elementsInCol / 2f;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
direction.X = (growthDirections & GrowthDirections.Right) != 0 ? 1 : -1;
|
||||
direction.Y = (growthDirections & GrowthDirections.Down) != 0 ? 1 : -1;
|
||||
offset.X = direction.X == 1 ? 0 : -iconSize.X;
|
||||
offset.Y = direction.Y == 1 ? 0 : -iconSize.Y;
|
||||
}
|
||||
}
|
||||
|
||||
public static Vector2 CalculateStartPosition(Vector2 position, Vector2 size, GrowthDirections growthDirections)
|
||||
{
|
||||
Vector2 area = size;
|
||||
if ((growthDirections & GrowthDirections.Left) != 0)
|
||||
{
|
||||
area.X = -area.X;
|
||||
}
|
||||
|
||||
if ((growthDirections & GrowthDirections.Up) != 0)
|
||||
{
|
||||
area.Y = -area.Y;
|
||||
}
|
||||
|
||||
Vector2 startPos = position;
|
||||
if ((growthDirections & GrowthDirections.Centered) != 0)
|
||||
{
|
||||
if ((growthDirections & GrowthDirections.Up) != 0 || (growthDirections & GrowthDirections.Down) != 0)
|
||||
{
|
||||
startPos.X = position.X - size.X / 2f;
|
||||
}
|
||||
else if ((growthDirections & GrowthDirections.Left) != 0 || (growthDirections & GrowthDirections.Right) != 0)
|
||||
{
|
||||
startPos.Y = position.Y - size.Y / 2f;
|
||||
}
|
||||
}
|
||||
|
||||
Vector2 endPos = position + area;
|
||||
|
||||
if (endPos.X < position.X)
|
||||
{
|
||||
startPos.X = endPos.X;
|
||||
}
|
||||
|
||||
if (endPos.Y < position.Y)
|
||||
{
|
||||
startPos.Y = endPos.Y;
|
||||
}
|
||||
|
||||
return startPos;
|
||||
}
|
||||
|
||||
public static (List<Vector2>, Vector2, Vector2) CalculateIconPositions(
|
||||
GrowthDirections directions,
|
||||
uint count,
|
||||
Vector2 position,
|
||||
Vector2 size,
|
||||
Vector2 iconSize,
|
||||
Vector2 iconPadding,
|
||||
bool fillRowsFirst,
|
||||
LayoutInfo layoutInfo)
|
||||
{
|
||||
List<Vector2> list = new List<Vector2>();
|
||||
Vector2 minPos = new Vector2(float.MaxValue, float.MaxValue);
|
||||
Vector2 maxPos = Vector2.Zero;
|
||||
|
||||
int row = 0;
|
||||
int col = 0;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
CalculateAxisDirections(
|
||||
directions,
|
||||
row,
|
||||
col,
|
||||
count,
|
||||
size,
|
||||
iconSize,
|
||||
iconPadding,
|
||||
out Vector2 direction,
|
||||
out Vector2 offset
|
||||
);
|
||||
|
||||
Vector2 pos = new Vector2(
|
||||
position.X + offset.X + iconSize.X * col * direction.X + iconPadding.X * col * direction.X,
|
||||
position.Y + offset.Y + iconSize.Y * row * direction.Y + iconPadding.Y * row * direction.Y
|
||||
);
|
||||
|
||||
minPos.X = Math.Min(pos.X, minPos.X);
|
||||
minPos.Y = Math.Min(pos.Y, minPos.Y);
|
||||
maxPos.X = Math.Max(pos.X + iconSize.X, maxPos.X);
|
||||
maxPos.Y = Math.Max(pos.Y + iconSize.Y, maxPos.Y);
|
||||
|
||||
list.Add(pos);
|
||||
|
||||
// rows / columns
|
||||
if (fillRowsFirst)
|
||||
{
|
||||
col += 1;
|
||||
if (col >= layoutInfo.TotalColCount)
|
||||
{
|
||||
col = 0;
|
||||
row += 1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
row += 1;
|
||||
if (row >= layoutInfo.TotalRowCount)
|
||||
{
|
||||
row = 0;
|
||||
col += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (list, minPos, maxPos);
|
||||
}
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum GrowthDirections : short
|
||||
{
|
||||
Up = 1,
|
||||
Down = 2,
|
||||
Left = 4,
|
||||
Right = 8,
|
||||
Centered = 16,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
Copyright(c) 2021 talimity (https://github.com/talimity/mptimer)
|
||||
Modifications Copyright(c) 2021 HSUI
|
||||
08/29/2021 - Mostly using original's code with minimal adaptations
|
||||
for HSUI.
|
||||
|
||||
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.Bindings.ImGui;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace HSUI.Helpers
|
||||
{
|
||||
internal class MPTickHelper : IDisposable
|
||||
{
|
||||
public const double ServerTickRate = 3;
|
||||
protected const float PollingRate = 1 / 30f;
|
||||
private int _lastMpValue = -1;
|
||||
protected double LastTickTime;
|
||||
protected double LastUpdate;
|
||||
|
||||
public MPTickHelper()
|
||||
{
|
||||
Plugin.Framework.Update += FrameworkOnOnUpdateEvent;
|
||||
}
|
||||
|
||||
public double LastTick => LastTickTime;
|
||||
|
||||
private void FrameworkOnOnUpdateEvent(IFramework framework)
|
||||
{
|
||||
var player = Plugin.ObjectTable.LocalPlayer;
|
||||
if (player is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var now = ImGui.GetTime();
|
||||
if (now - LastUpdate < PollingRate)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
LastUpdate = now;
|
||||
|
||||
var mp = player.CurrentMp;
|
||||
|
||||
// account for lucid dreaming screwing up mp calculations
|
||||
var lucidDreamingActive = Utils.StatusListForBattleChara(player).Any(e => e.StatusId == 1204);
|
||||
|
||||
if (!lucidDreamingActive && _lastMpValue < mp)
|
||||
{
|
||||
LastTickTime = now;
|
||||
}
|
||||
else if (LastTickTime + ServerTickRate <= now)
|
||||
{
|
||||
LastTickTime += ServerTickRate;
|
||||
}
|
||||
|
||||
_lastMpValue = (int)mp;
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Plugin.Framework.Update -= FrameworkOnOnUpdateEvent;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
~MPTickHelper()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
|
||||
namespace HSUI.Helpers
|
||||
{
|
||||
internal class PetRenamerHelper
|
||||
{
|
||||
private Dictionary<ulong, string>? PetNicknamesDictionary;
|
||||
|
||||
#region Singleton
|
||||
public static void Initialize() { Instance = new PetRenamerHelper(); }
|
||||
|
||||
public static PetRenamerHelper Instance { get; private set; } = null!;
|
||||
|
||||
public PetRenamerHelper()
|
||||
{
|
||||
AssignShares();
|
||||
}
|
||||
|
||||
~PetRenamerHelper()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Plugin.PluginInterface.RelinquishData("PetRenamer.GameObjectRenameDict");
|
||||
|
||||
Instance = null!;
|
||||
}
|
||||
#endregion
|
||||
|
||||
private void AssignShares()
|
||||
{
|
||||
try
|
||||
{
|
||||
PetNicknamesDictionary = Plugin.PluginInterface.GetOrCreateData("PetRenamer.GameObjectRenameDict", () => new Dictionary<ulong, string>());
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private string? GetNameForActor(IGameObject actor)
|
||||
{
|
||||
if (PetNicknamesDictionary == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (PetNicknamesDictionary.TryGetValue(actor.GameObjectId, out string? nickname))
|
||||
{
|
||||
return nickname;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string? GetPetName(IGameObject? actor)
|
||||
{
|
||||
if (actor == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (actor.ObjectKind != ObjectKind.Companion && actor.ObjectKind != ObjectKind.BattleNpc)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return GetNameForActor(actor);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
Copyright(c) 2021 xorus (https://github.com/xorus/EngageTimer)
|
||||
Modifications Copyright(c) 2021 HSUI
|
||||
09/21/2021 - Extracted code to hook the game's pulltimer functions.
|
||||
|
||||
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 System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Logging;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
|
||||
namespace HSUI.Helpers
|
||||
{
|
||||
public unsafe class PullTimerHelper
|
||||
{
|
||||
#region Singleton
|
||||
private PullTimerHelper()
|
||||
{
|
||||
PullTimerState = new PullTimerState();
|
||||
|
||||
try
|
||||
{
|
||||
_countdownTimerHook = Plugin.GameInteropProvider.HookFromAddress<AgentInterface.Delegates.Update>(
|
||||
AgentModule.Instance()->GetAgentByInternalId(AgentId.CountDownSettingDialog)->VirtualTable->Update,
|
||||
CountdownTimerFunc);
|
||||
_countdownTimerHook?.Enable();
|
||||
}
|
||||
catch
|
||||
{
|
||||
Plugin.Logger.Error("PullTimeHelper CountdownTimer Hook failed!!!");
|
||||
}
|
||||
}
|
||||
public static void Initialize() { Instance = new PullTimerHelper(); }
|
||||
public static PullTimerHelper Instance { get; private set; } = null!;
|
||||
|
||||
~PullTimerHelper()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_countdownTimerHook?.Disable();
|
||||
_countdownTimerHook?.Dispose();
|
||||
|
||||
Instance = null!;
|
||||
}
|
||||
#endregion
|
||||
|
||||
private DateTime _combatTimeEnd;
|
||||
private DateTime _combatTimeStart;
|
||||
|
||||
private ulong _agentData;
|
||||
public bool CountDownRunning;
|
||||
|
||||
private int _countDownStallTicks;
|
||||
|
||||
private readonly Hook<AgentInterface.Delegates.Update>? _countdownTimerHook;
|
||||
public float LastCountDownValue;
|
||||
private bool _shouldRestartCombatTimer = true;
|
||||
private bool _lastMaxValueSet = false;
|
||||
|
||||
public readonly PullTimerState PullTimerState;
|
||||
|
||||
public void Update()
|
||||
{
|
||||
if (PullTimerState.Mocked)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateCountDown();
|
||||
UpdateEncounterTimer();
|
||||
PullTimerState.InInstance = Plugin.Condition[ConditionFlag.BoundByDuty];
|
||||
}
|
||||
|
||||
private void CountdownTimerFunc(AgentInterface* agentInterface, uint frameCount)
|
||||
{
|
||||
_agentData = (ulong)agentInterface;
|
||||
_countdownTimerHook?.Original(agentInterface, frameCount);
|
||||
}
|
||||
|
||||
private void UpdateEncounterTimer()
|
||||
{
|
||||
if (Plugin.Condition[ConditionFlag.InCombat])
|
||||
{
|
||||
PullTimerState.InCombat = true;
|
||||
if (_shouldRestartCombatTimer)
|
||||
{
|
||||
_shouldRestartCombatTimer = false;
|
||||
_combatTimeStart = DateTime.Now;
|
||||
}
|
||||
|
||||
_combatTimeEnd = DateTime.Now;
|
||||
}
|
||||
else
|
||||
{
|
||||
PullTimerState.InCombat = false;
|
||||
_shouldRestartCombatTimer = true;
|
||||
}
|
||||
|
||||
PullTimerState.CombatStart = _combatTimeStart;
|
||||
PullTimerState.CombatDuration = _combatTimeEnd - _combatTimeStart;
|
||||
PullTimerState.CombatEnd = _combatTimeEnd;
|
||||
}
|
||||
|
||||
private void UpdateCountDown()
|
||||
{
|
||||
PullTimerState.CountingDown = false;
|
||||
|
||||
if (_agentData == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
byte countdownActive = Marshal.PtrToStructure<byte>((IntPtr)_agentData + 0x38);
|
||||
if (countdownActive == 0)
|
||||
{
|
||||
_lastMaxValueSet = false;
|
||||
return;
|
||||
}
|
||||
|
||||
float countDownPointerValue = Marshal.PtrToStructure<float>((IntPtr)_agentData + 0x2c);
|
||||
|
||||
// is last value close enough (workaround for floating point approx)
|
||||
if (Math.Abs(countDownPointerValue - LastCountDownValue) < 0.001f)
|
||||
{
|
||||
_countDownStallTicks++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_countDownStallTicks = 0;
|
||||
CountDownRunning = true;
|
||||
}
|
||||
|
||||
if (_countDownStallTicks > 50)
|
||||
{
|
||||
CountDownRunning = false;
|
||||
}
|
||||
|
||||
if (countDownPointerValue > 0 && CountDownRunning)
|
||||
{
|
||||
PullTimerState.CountDownValue = countDownPointerValue;
|
||||
PullTimerState.CountingDown = true;
|
||||
}
|
||||
|
||||
if (!_lastMaxValueSet && CountDownRunning)
|
||||
{
|
||||
PullTimerState.CountDownMax = countDownPointerValue;
|
||||
_lastMaxValueSet = true;
|
||||
}
|
||||
else if (_lastMaxValueSet && countDownPointerValue <= 0)
|
||||
{
|
||||
_lastMaxValueSet = false;
|
||||
}
|
||||
|
||||
LastCountDownValue = countDownPointerValue;
|
||||
}
|
||||
}
|
||||
|
||||
public class PullTimerState
|
||||
{
|
||||
private bool _inCombat;
|
||||
private bool _countingDown;
|
||||
public TimeSpan CombatDuration { get; set; }
|
||||
public DateTime CombatEnd { get; set; }
|
||||
public DateTime CombatStart { get; set; }
|
||||
|
||||
public bool Mocked { get; set; }
|
||||
|
||||
public bool InCombat
|
||||
{
|
||||
get => _inCombat;
|
||||
set
|
||||
{
|
||||
if (_inCombat == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_inCombat = value;
|
||||
InCombatChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public bool CountingDown
|
||||
{
|
||||
get => _countingDown;
|
||||
set
|
||||
{
|
||||
if (_countingDown == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_countingDown = value;
|
||||
CountingDownChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public bool InInstance { get; set; }
|
||||
|
||||
public float CountDownValue { get; set; } = 0f;
|
||||
public float CountDownMax { get; set; } = 0f;
|
||||
public event EventHandler? InCombatChanged;
|
||||
public event EventHandler? CountingDownChanged;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace HSUI.Helpers
|
||||
{
|
||||
public class SmoothHPHelper
|
||||
{
|
||||
private float? _startHp;
|
||||
private float? _targetHp;
|
||||
private float? _lastHp;
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_startHp = null;
|
||||
_targetHp = null;
|
||||
_lastHp = null;
|
||||
}
|
||||
|
||||
public uint GetNextHp(int currentHp, int maxHp, float velocity)
|
||||
{
|
||||
if (!_startHp.HasValue || !_targetHp.HasValue || !_lastHp.HasValue)
|
||||
{
|
||||
_lastHp = currentHp;
|
||||
_startHp = currentHp;
|
||||
_targetHp = currentHp;
|
||||
}
|
||||
|
||||
if (currentHp != _lastHp)
|
||||
{
|
||||
_startHp = _lastHp;
|
||||
_targetHp = currentHp;
|
||||
}
|
||||
|
||||
if (_startHp.HasValue && _targetHp.HasValue)
|
||||
{
|
||||
float delta = _targetHp.Value - _startHp.Value;
|
||||
float offset = delta * velocity / 100f;
|
||||
_startHp = Math.Clamp(_startHp.Value + offset, 0, maxHp);
|
||||
}
|
||||
|
||||
_lastHp = currentHp;
|
||||
return _startHp.HasValue ? (uint)_startHp.Value : (uint)currentHp;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using System;
|
||||
|
||||
namespace HSUI.Helpers
|
||||
{
|
||||
internal class SpellHelper
|
||||
{
|
||||
#region Singleton
|
||||
private static Lazy<SpellHelper> _lazyInstance = new Lazy<SpellHelper>(() => new SpellHelper());
|
||||
|
||||
public static SpellHelper Instance => _lazyInstance.Value;
|
||||
|
||||
~SpellHelper()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lazyInstance = new Lazy<SpellHelper>(() => new SpellHelper());
|
||||
}
|
||||
#endregion
|
||||
|
||||
private readonly unsafe ActionManager* _actionManager;
|
||||
|
||||
public unsafe SpellHelper()
|
||||
{
|
||||
_actionManager = ActionManager.Instance();
|
||||
}
|
||||
|
||||
public unsafe uint GetSpellActionId(uint actionId) => _actionManager->GetAdjustedActionId(actionId);
|
||||
|
||||
public unsafe float GetRecastTimeElapsed(uint actionId) => _actionManager->GetRecastTimeElapsed(ActionType.Action, GetSpellActionId(actionId));
|
||||
public unsafe float GetRealRecastTimeElapsed(uint actionId) => _actionManager->GetRecastTimeElapsed(ActionType.Action, actionId);
|
||||
|
||||
public unsafe float GetRecastTime(uint actionId) => _actionManager->GetRecastTime(ActionType.Action, GetSpellActionId(actionId));
|
||||
public unsafe float GetRealRecastTime(uint actionId) => _actionManager->GetRecastTime(ActionType.Action, actionId);
|
||||
|
||||
public unsafe uint GetLastUsedActionId() => _actionManager->Combo.Action;
|
||||
|
||||
public float GetSpellCooldown(uint actionId) => Math.Abs(GetRecastTime(GetSpellActionId(actionId)) - GetRecastTimeElapsed(GetSpellActionId(actionId)));
|
||||
public float GetRealSpellCooldown(uint actionId) => Math.Abs(GetRealRecastTime(actionId) - GetRealRecastTimeElapsed(actionId));
|
||||
|
||||
public int GetSpellCooldownInt(uint actionId)
|
||||
{
|
||||
int cooldown = (int)Math.Ceiling(GetSpellCooldown(actionId) % GetRecastTime(actionId));
|
||||
return Math.Max(0, cooldown);
|
||||
}
|
||||
|
||||
public int GetStackCount(int maxStacks, uint actionId)
|
||||
{
|
||||
int cooldown = GetSpellCooldownInt(actionId);
|
||||
float recastTime = GetRecastTime(actionId);
|
||||
|
||||
if (cooldown <= 0 || recastTime == 0)
|
||||
{
|
||||
return maxStacks;
|
||||
}
|
||||
|
||||
return maxStacks - (int)Math.Ceiling(cooldown / (recastTime / maxStacks));
|
||||
}
|
||||
|
||||
public unsafe bool IsActionHighlighted(uint actionId, ActionType type = ActionType.Action) => _actionManager->IsActionHighlighted(type, actionId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,531 @@
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using HSUI.Config;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using StructsBattleChara = FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara;
|
||||
|
||||
namespace HSUI.Helpers
|
||||
{
|
||||
public static class TextTagsHelper
|
||||
{
|
||||
public static void Initialize()
|
||||
{
|
||||
foreach (string key in HealthTextTags.Keys)
|
||||
{
|
||||
CharaTextTags.Add(key, (chara) => HealthTextTags[key](chara.CurrentHp, chara.MaxHp));
|
||||
}
|
||||
|
||||
foreach (string key in ManaTextTags.Keys)
|
||||
{
|
||||
CharaTextTags.Add(key, (chara) => ManaTextTags[key](JobsHelper.CurrentPrimaryResource(chara), JobsHelper.MaxPrimaryResource(chara)));
|
||||
}
|
||||
}
|
||||
|
||||
public static Dictionary<string, Func<IGameObject?, string?, int, bool?, string>> TextTags = new Dictionary<string, Func<IGameObject?, string?, int, bool?, string>>()
|
||||
{
|
||||
#region generic names
|
||||
["[name]"] = (actor, name, length, isPlayerName) =>
|
||||
ValidateName(actor, name).
|
||||
Truncated(length).
|
||||
CheckForUpperCase(),
|
||||
|
||||
["[name:first]"] = (actor, name, length, isPlayerName) =>
|
||||
ValidateName(actor, name).
|
||||
FirstName().
|
||||
Truncated(length).
|
||||
CheckForUpperCase(),
|
||||
|
||||
["[name:last]"] = (actor, name, length, isPlayerName) =>
|
||||
ValidateName(actor, name).
|
||||
LastName().
|
||||
Truncated(length).
|
||||
CheckForUpperCase(),
|
||||
|
||||
["[name:initials]"] = (actor, name, length, isPlayerName) =>
|
||||
ValidateName(actor, name).
|
||||
Initials().
|
||||
Truncated(length).
|
||||
CheckForUpperCase(),
|
||||
|
||||
["[name:abbreviate]"] = (actor, name, length, isPlayerName) =>
|
||||
ValidateName(actor, name).
|
||||
Abbreviate().
|
||||
CheckForUpperCase(),
|
||||
#endregion
|
||||
|
||||
#region player names
|
||||
["[player_name]"] = (actor, name, length, isPlayerName) =>
|
||||
ValidatePlayerName(actor, name, isPlayerName).
|
||||
Truncated(length).
|
||||
CheckForUpperCase(),
|
||||
|
||||
["[player_name:first]"] = (actor, name, length, isPlayerName) =>
|
||||
ValidatePlayerName(actor, name, isPlayerName).
|
||||
FirstName().
|
||||
Truncated(length).
|
||||
CheckForUpperCase(),
|
||||
|
||||
["[player_name:last]"] = (actor, name, length, isPlayerName) =>
|
||||
ValidatePlayerName(actor, name, isPlayerName).
|
||||
LastName().
|
||||
Truncated(length).
|
||||
CheckForUpperCase(),
|
||||
|
||||
["[player_name:initials]"] = (actor, name, length, isPlayerName) =>
|
||||
ValidatePlayerName(actor, name, isPlayerName).
|
||||
Initials().
|
||||
Truncated(length).
|
||||
CheckForUpperCase(),
|
||||
#endregion
|
||||
|
||||
#region npc names
|
||||
["[npc_name]"] = (actor, name, length, isPlayerName) =>
|
||||
ValidateNPCName(actor, name, isPlayerName).
|
||||
CheckForUpperCase(),
|
||||
|
||||
["[npc_name:first]"] = (actor, name, length, isPlayerName) =>
|
||||
ValidateNPCName(actor, name, isPlayerName).
|
||||
FirstName().
|
||||
CheckForUpperCase(),
|
||||
|
||||
["[npc_name:last]"] = (actor, name, length, isPlayerName) =>
|
||||
ValidateNPCName(actor, name, isPlayerName).
|
||||
LastName().
|
||||
CheckForUpperCase(),
|
||||
|
||||
["[npc_name:initials]"] = (actor, name, length, isPlayerName) =>
|
||||
ValidateNPCName(actor, name, isPlayerName).
|
||||
Initials().
|
||||
CheckForUpperCase(),
|
||||
#endregion
|
||||
};
|
||||
|
||||
public static Dictionary<string, Func<IGameObject?, string?, string>> ExpTags = new Dictionary<string, Func<IGameObject?, string?, string>>()
|
||||
{
|
||||
#region experience
|
||||
["[exp:current]"] = (actor, name) => ExperienceHelper.Instance.CurrentExp.ToString(),
|
||||
|
||||
["[exp:current-formatted]"] = (actor, name) => ExperienceHelper.Instance.CurrentExp.ToString("N0", ConfigurationManager.Instance.ActiveCultreInfo),
|
||||
|
||||
["[exp:current-short]"] = (actor, name) => ExperienceHelper.Instance.CurrentExp.KiloFormat(),
|
||||
|
||||
["[exp:required]"] = (actor, name) => ExperienceHelper.Instance.RequiredExp.ToString(),
|
||||
|
||||
["[exp:required-formatted]"] = (actor, name) => ExperienceHelper.Instance.RequiredExp.ToString("N0", ConfigurationManager.Instance.ActiveCultreInfo),
|
||||
|
||||
["[exp:required-short]"] = (actor, name) => ExperienceHelper.Instance.RequiredExp.KiloFormat(),
|
||||
|
||||
["[exp:required-to-level]"] = (actor, name) => (ExperienceHelper.Instance.RequiredExp - ExperienceHelper.Instance.CurrentExp).ToString(),
|
||||
|
||||
["[exp:required-to-level-formatted]"] = (actor, name) => (ExperienceHelper.Instance.RequiredExp - ExperienceHelper.Instance.CurrentExp).ToString("N0", ConfigurationManager.Instance.ActiveCultreInfo),
|
||||
|
||||
["[exp:required-to-level-short]"] = (actor, name) => (ExperienceHelper.Instance.RequiredExp - ExperienceHelper.Instance.CurrentExp).KiloFormat(),
|
||||
|
||||
["[exp:rested]"] = (actor, name) => ExperienceHelper.Instance.RestedExp.ToString(),
|
||||
|
||||
["[exp:rested-formatted]"] = (actor, name) => ExperienceHelper.Instance.RestedExp.ToString("N0", ConfigurationManager.Instance.ActiveCultreInfo),
|
||||
|
||||
["[exp:rested-short]"] = (actor, name) => ExperienceHelper.Instance.RestedExp.KiloFormat(),
|
||||
|
||||
["[exp:percent]"] = (actor, name) => ExperienceHelper.Instance.PercentExp.ToString("N0"),
|
||||
|
||||
["[exp:percent-decimal]"] = (actor, name) => ExperienceHelper.Instance.PercentExp.ToString("N1", ConfigurationManager.Instance.ActiveCultreInfo),
|
||||
#endregion
|
||||
};
|
||||
|
||||
public static Dictionary<string, Func<uint, uint, string>> HealthTextTags = new Dictionary<string, Func<uint, uint, string>>()
|
||||
{
|
||||
#region health
|
||||
["[health:current]"] = (currentHp, maxHp) => currentHp.ToString(),
|
||||
|
||||
["[health:current-formatted]"] = (currentHp, maxHp) => currentHp.ToString("N0", ConfigurationManager.Instance.ActiveCultreInfo),
|
||||
|
||||
["[health:current-short]"] = (currentHp, maxHp) => currentHp.KiloFormat(),
|
||||
|
||||
["[health:current-percent]"] = (currentHp, maxHp) => currentHp == maxHp ? "100" : (100f * currentHp / Math.Max(1, maxHp)).ToString("N0"),
|
||||
|
||||
["[health:current-percent-short]"] = (currentHp, maxHp) => currentHp == maxHp ? currentHp.KiloFormat() : (100f * currentHp / Math.Max(1, maxHp)).ToString("N0"),
|
||||
|
||||
["[health:max]"] = (currentHp, maxHp) => maxHp.ToString(),
|
||||
|
||||
["[health:max-formatted]"] = (currentHp, maxHp) => maxHp.ToString("N0", ConfigurationManager.Instance.ActiveCultreInfo),
|
||||
|
||||
["[health:max-short]"] = (currentHp, maxHp) => maxHp.KiloFormat(),
|
||||
|
||||
["[health:percent]"] = (currentHp, maxHp) => (100f * currentHp / Math.Max(1, maxHp)).ToString("N0", ConfigurationManager.Instance.ActiveCultreInfo),
|
||||
|
||||
["[health:percent-hidden]"] = (currentHp, maxHp) => currentHp == (0 | maxHp) ? "" : (100f * currentHp / Math.Max(1, maxHp)).ToString("N0", ConfigurationManager.Instance.ActiveCultreInfo),
|
||||
|
||||
["[health:percent-decimal]"] = (currentHp, maxHp) => (100f * currentHp / Math.Max(1f, maxHp)).ToString("N1", ConfigurationManager.Instance.ActiveCultreInfo),
|
||||
|
||||
["[health:percent-decimal-uniform]"] = (currentHp, maxHp) => ConsistentDigitPercentage(currentHp, maxHp),
|
||||
|
||||
["[health:deficit]"] = (currentHp, maxHp) => currentHp == maxHp ? "0" : $"-{maxHp - currentHp}",
|
||||
|
||||
["[health:deficit-formatted]"] = (currentHp, maxHp) => currentHp == maxHp ? "0" : "-" + (maxHp - currentHp).ToString("N0", ConfigurationManager.Instance.ActiveCultreInfo),
|
||||
|
||||
["[health:deficit-short]"] = (currentHp, maxHp) => currentHp == maxHp ? "0" : $"-{(maxHp - currentHp).KiloFormat()}",
|
||||
#endregion
|
||||
};
|
||||
|
||||
public static Dictionary<string, Func<uint, uint, string>> ManaTextTags = new Dictionary<string, Func<uint, uint, string>>()
|
||||
{
|
||||
#region mana
|
||||
["[mana:current]"] = (currentMp, maxMp) => currentMp.ToString(),
|
||||
|
||||
["[mana:current-formatted]"] = (currentMp, maxMp) => currentMp.ToString("N0", ConfigurationManager.Instance.ActiveCultreInfo),
|
||||
|
||||
["[mana:current-short]"] = (currentMp, maxMp) => currentMp.KiloFormat(),
|
||||
|
||||
["[mana:current-percent]"] = (currentMp, maxMp) => currentMp == maxMp ? "100" : (100f * currentMp / Math.Max(1, maxMp)).ToString("N0"),
|
||||
|
||||
["[mana:current-percent-short]"] = (currentMp, maxMp) => currentMp == maxMp ? currentMp.KiloFormat() : (100f * currentMp / Math.Max(1, maxMp)).ToString("N0"),
|
||||
|
||||
["[mana:max]"] = (currentMp, maxMp) => maxMp.ToString(),
|
||||
|
||||
["[mana:max-formatted]"] = (currentMp, maxMp) => maxMp.ToString("N0", ConfigurationManager.Instance.ActiveCultreInfo),
|
||||
|
||||
["[mana:max-short]"] = (currentMp, maxMp) => maxMp.KiloFormat(),
|
||||
|
||||
["[mana:percent]"] = (currentMp, maxMp) => (100f * currentMp / Math.Max(1, maxMp)).ToString("N0", ConfigurationManager.Instance.ActiveCultreInfo),
|
||||
|
||||
["[mana:percent-decimal]"] = (currentMp, maxMp) => (100f * currentMp / Math.Max(1, maxMp)).ToString("N1", ConfigurationManager.Instance.ActiveCultreInfo),
|
||||
|
||||
["[mana:percent-decimal-uniform]"] = (currentMp, maxMp) => ConsistentDigitPercentage(currentMp, maxMp),
|
||||
|
||||
["[mana:deficit]"] = (currentMp, maxMp) => currentMp == maxMp ? "0" : $"-{maxMp - currentMp}",
|
||||
|
||||
["[mana:deficit-formatted]"] = (currentMp, maxMp) => currentMp == maxMp ? "0" : "-" + (maxMp - currentMp).ToString("N0", ConfigurationManager.Instance.ActiveCultreInfo),
|
||||
|
||||
["[mana:deficit-short]"] = (currentMp, maxMp) => currentMp == maxMp ? "0" : $"-{(maxMp - currentMp).KiloFormat()}",
|
||||
#endregion
|
||||
};
|
||||
|
||||
public static Dictionary<string, Func<ICharacter, string>> CharaTextTags = new Dictionary<string, Func<ICharacter, string>>()
|
||||
{
|
||||
#region misc
|
||||
["[distance]"] = (chara) => (chara.YalmDistanceX + 1).ToString(),
|
||||
|
||||
["[company]"] = (chara) => chara.CompanyTag.ToString(),
|
||||
|
||||
["[company-formatted]"] = (chara) => !String.IsNullOrEmpty(chara.CompanyTag.ToString()) ? $"«{chara.CompanyTag}»" : "",
|
||||
|
||||
["[level]"] = (chara) => chara.Level > 0 ? chara.Level.ToString() : "-",
|
||||
|
||||
["[level:adjusted]"] = (chara) =>
|
||||
{
|
||||
if (chara is IBattleChara npc)
|
||||
{
|
||||
return GetZoneAdjustedLevel(npc, chara.Level);
|
||||
}
|
||||
|
||||
return chara.Level > 0 ? chara.Level.ToString() : "-";
|
||||
},
|
||||
|
||||
["[level:hidden]"] = (chara) => (chara.Level > 1 && chara.Level < 100) ? chara.Level.ToString() : "",
|
||||
|
||||
["[job]"] = (chara) => JobsHelper.JobNames.TryGetValue(chara.ClassJob.RowId, out var jobName) ? jobName : "",
|
||||
|
||||
["[job-full]"] = (chara) => JobsHelper.JobFullNames.TryGetValue(chara.ClassJob.RowId, out var jobName) ? jobName : "",
|
||||
|
||||
["[time-till-max-gp]"] = JobsHelper.TimeTillMaxGP,
|
||||
|
||||
["[chocobo-time]"] = (chara) =>
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
if (chara is IBattleNpc npc && npc.BattleNpcKind == BattleNpcSubKind.Chocobo)
|
||||
{
|
||||
float seconds = UIState.Instance()->Buddy.CompanionInfo.TimeLeft;
|
||||
if (seconds <= 0)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
TimeSpan time = TimeSpan.FromSeconds(seconds);
|
||||
return time.ToString(@"mm\:ss");
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
#endregion
|
||||
};
|
||||
|
||||
public static Dictionary<string, Func<string, int, string>> TitleTextTags = new Dictionary<string, Func<string, int, string>>()
|
||||
{
|
||||
#region title
|
||||
["[title]"] = (title, length) => title.Truncated(length).CheckForUpperCase(),
|
||||
|
||||
["[title:first]"] = (title, length) => title.FirstName().Truncated(length).CheckForUpperCase(),
|
||||
|
||||
["[title:last]"] = (title, length) => title.LastName().Truncated(length).CheckForUpperCase(),
|
||||
|
||||
["[title:initials]"] = (title, length) => title.Initials().Truncated(length).CheckForUpperCase(),
|
||||
#endregion
|
||||
};
|
||||
|
||||
private static List<Dictionary<string, Func<uint, uint, string>>> NumericValuesTagMaps = new List<Dictionary<string, Func<uint, uint, string>>>()
|
||||
{
|
||||
HealthTextTags,
|
||||
ManaTextTags
|
||||
};
|
||||
|
||||
private static string ReplaceTagWithString(
|
||||
string tag,
|
||||
IGameObject? actor,
|
||||
string? name = null,
|
||||
uint? current = null,
|
||||
uint? max = null,
|
||||
bool? isPlayerName = null,
|
||||
string? title = null)
|
||||
{
|
||||
int length = 0;
|
||||
ParseLength(ref tag, ref length);
|
||||
|
||||
if (TextTags.TryGetValue(tag, out Func<IGameObject?, string?, int, bool?, string>? func) && func != null)
|
||||
{
|
||||
return func(actor, name, length, isPlayerName);
|
||||
}
|
||||
|
||||
if (ExpTags.TryGetValue(tag, out Func<IGameObject?, string?, string>? expFunc) && expFunc != null)
|
||||
{
|
||||
return expFunc(actor, name);
|
||||
}
|
||||
|
||||
if (actor is ICharacter chara &&
|
||||
CharaTextTags.TryGetValue(tag, out Func<ICharacter, string>? charaFunc) && charaFunc != null)
|
||||
{
|
||||
return charaFunc(chara);
|
||||
}
|
||||
else if (current.HasValue && max.HasValue)
|
||||
{
|
||||
foreach (var map in NumericValuesTagMaps)
|
||||
{
|
||||
if (map.TryGetValue(tag, out Func<uint, uint, string>? numericFunc) && numericFunc != null)
|
||||
{
|
||||
return numericFunc(current.Value, max.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (title != null &&
|
||||
TitleTextTags.TryGetValue(tag, out Func<string, int, string>? titlefunc) && titlefunc != null)
|
||||
{
|
||||
return titlefunc(title, length);
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
public static string FormattedText(
|
||||
string text,
|
||||
IGameObject? actor,
|
||||
string? name = null,
|
||||
uint? current = null,
|
||||
uint? max = null,
|
||||
bool? isPlayerName = null,
|
||||
string? title = null)
|
||||
{
|
||||
bool isPlayer = (isPlayerName.HasValue && isPlayerName.Value == true) ||
|
||||
(actor != null && actor.ObjectKind == ObjectKind.Player);
|
||||
|
||||
try
|
||||
{
|
||||
// grouping
|
||||
List<string> groups = ParseGroups(text);
|
||||
string result = "";
|
||||
|
||||
foreach (string group in groups)
|
||||
{
|
||||
// tags
|
||||
string groupText = ParseGroup(group, isPlayer);
|
||||
|
||||
MatchCollection matches = Regex.Matches(groupText, @"\[(.*?)\]");
|
||||
string formattedGroupText = matches.Aggregate(groupText, (c, m) =>
|
||||
{
|
||||
string formattedText = ReplaceTagWithString(m.Value, actor, name, current, max, isPlayerName, title);
|
||||
return c.Replace(m.Value, formattedText);
|
||||
});
|
||||
|
||||
result += formattedGroupText;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Plugin.Logger.Error(e.Message);
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<string> ParseGroups(string text)
|
||||
{
|
||||
MatchCollection matches = Regex.Matches(text, @"\{(.*?)\}");
|
||||
if (matches.Count == 0)
|
||||
{
|
||||
return new List<string>() { text };
|
||||
}
|
||||
|
||||
List<string> result = new List<string>();
|
||||
int index = 0;
|
||||
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
if (index < match.Index)
|
||||
{
|
||||
result.Add(text.Substring(0, match.Index - index));
|
||||
}
|
||||
|
||||
result.Add(text.Substring(match.Index, match.Length));
|
||||
index = match.Index + match.Length;
|
||||
}
|
||||
|
||||
if (index < text.Length)
|
||||
{
|
||||
result.Add(text.Substring(index));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string ParseGroup(string text, bool isPlayer)
|
||||
{
|
||||
if (!text.Contains("="))
|
||||
{
|
||||
return text;
|
||||
}
|
||||
|
||||
if (isPlayer)
|
||||
{
|
||||
if (text.StartsWith("{player="))
|
||||
{
|
||||
text = text.Substring(8);
|
||||
}
|
||||
else
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (text.StartsWith("{npc="))
|
||||
{
|
||||
text = text.Substring(5);
|
||||
}
|
||||
else
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
int groupEndIndex = text.IndexOf("}");
|
||||
if (groupEndIndex > 0)
|
||||
{
|
||||
text = text.Remove(groupEndIndex, 1);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
private static void ParseLength(ref string tag, ref int length)
|
||||
{
|
||||
int index = tag.IndexOf(".");
|
||||
if (index != -1)
|
||||
{
|
||||
string lengthString = tag.Substring(index + 1);
|
||||
lengthString = lengthString.Substring(0, lengthString.Length - 1);
|
||||
|
||||
try
|
||||
{
|
||||
length = int.Parse(lengthString);
|
||||
}
|
||||
catch { }
|
||||
|
||||
tag = tag.Substring(0, tag.Length - lengthString.Length - 2) + "]";
|
||||
}
|
||||
}
|
||||
|
||||
private static string ValidateName(IGameObject? actor, string? name)
|
||||
{
|
||||
string? n = actor?.Name.ToString() ?? name;
|
||||
|
||||
// Detour for PetRenamer
|
||||
try
|
||||
{
|
||||
string? customPetName = PetRenamerHelper.Instance.GetPetName(actor);
|
||||
n = customPetName ?? n;
|
||||
}
|
||||
catch { }
|
||||
|
||||
return (n == null || n == "") ? "" : n;
|
||||
}
|
||||
|
||||
private static string ValidatePlayerName(IGameObject? actor, string? name, bool? isPlayerName = null)
|
||||
{
|
||||
if (isPlayerName.HasValue && isPlayerName.Value == false)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
else if (!isPlayerName.HasValue && actor?.ObjectKind != ObjectKind.Player)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
return ValidateName(actor, name);
|
||||
}
|
||||
|
||||
private static string ValidateNPCName(IGameObject? actor, string? name, bool? isPlayerName = null)
|
||||
{
|
||||
if (isPlayerName.HasValue && isPlayerName.Value == true)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
else if (!isPlayerName.HasValue && actor?.ObjectKind == ObjectKind.Player)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
return ValidateName(actor, name);
|
||||
}
|
||||
|
||||
private static string ConsistentDigitPercentage(float currentVal, float maxVal)
|
||||
{
|
||||
var rawPercentage = 100f * currentVal / Math.Max(1f, maxVal);
|
||||
return rawPercentage >= 100 || rawPercentage <= 0 ? rawPercentage.ToString("N0") : rawPercentage.ToString("N1");
|
||||
}
|
||||
|
||||
private static string GetZoneAdjustedLevel(IBattleChara? npc, int fallbackLevel)
|
||||
{
|
||||
if (npc == null)
|
||||
{
|
||||
return fallbackLevel > 0 ? fallbackLevel.ToString() : "-";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
StructsBattleChara* battleChara = (StructsBattleChara*)npc.Address;
|
||||
ForayInfo forayInfo = battleChara->ForayInfo;
|
||||
|
||||
if (forayInfo.Level > 0)
|
||||
{
|
||||
return forayInfo.Level.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
Plugin.Logger.Error("Error in getting ZoneAdjustedLevel");
|
||||
}
|
||||
|
||||
return fallbackLevel > 0 ? fallbackLevel.ToString() : "-";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Interface.Textures;
|
||||
using Dalamud.Interface.Textures.TextureWraps;
|
||||
using Lumina.Excel;
|
||||
using static Dalamud.Plugin.Services.ITextureProvider;
|
||||
|
||||
namespace HSUI.Helpers
|
||||
{
|
||||
public class TexturesHelper
|
||||
{
|
||||
public static IDalamudTextureWrap? GetTexture<T>(uint rowId, uint stackCount = 0, bool hdIcon = true) where T : struct, IExcelRow<T>
|
||||
{
|
||||
ExcelSheet<T> sheet = Plugin.DataManager.GetExcelSheet<T>();
|
||||
return sheet == null ? null : GetTexture<T>(sheet.GetRow(rowId), stackCount, hdIcon);
|
||||
}
|
||||
|
||||
public static IDalamudTextureWrap? GetTexture<T>(dynamic row, uint stackCount = 0, bool hdIcon = true) where T : struct, IExcelRow<T>
|
||||
{
|
||||
dynamic iconId = row.Icon;
|
||||
return GetTextureFromIconId(iconId, stackCount, hdIcon);
|
||||
}
|
||||
|
||||
public static IDalamudTextureWrap? GetTextureFromIconId(uint iconId, uint stackCount = 0, bool hdIcon = true)
|
||||
{
|
||||
GameIconLookup lookup = new GameIconLookup(iconId + stackCount, false, hdIcon);
|
||||
return Plugin.TextureProvider.GetFromGameIcon(lookup).GetWrapOrDefault();
|
||||
}
|
||||
|
||||
public static IDalamudTextureWrap? GetTextureFromPath(string path)
|
||||
{
|
||||
return Plugin.TextureProvider.GetFromGame(path).GetWrapOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility;
|
||||
using HSUI.Config;
|
||||
using HSUI.Config.Attributes;
|
||||
using HSUI.Interface.GeneralElements;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace HSUI.Helpers
|
||||
{
|
||||
public class TooltipsHelper : IDisposable
|
||||
{
|
||||
#region Singleton
|
||||
private TooltipsHelper()
|
||||
{
|
||||
}
|
||||
|
||||
public static void Initialize() { Instance = new TooltipsHelper(); }
|
||||
|
||||
public static TooltipsHelper Instance { get; private set; } = null!;
|
||||
|
||||
~TooltipsHelper()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Instance = null!;
|
||||
}
|
||||
#endregion
|
||||
|
||||
private float MaxWidth => 340 * ImGuiHelpers.GlobalScale;
|
||||
private float Margin => 5 * ImGuiHelpers.GlobalScale;
|
||||
|
||||
private TooltipsConfig _config => ConfigurationManager.Instance.GetConfigObject<TooltipsConfig>();
|
||||
|
||||
private string? _currentTooltipText = null;
|
||||
private Vector2 _textSize;
|
||||
private string? _currentTooltipTitle = null;
|
||||
private Vector2 _titleSize;
|
||||
private string? _previousRawText = null;
|
||||
|
||||
private Vector2 _position;
|
||||
private Vector2 _size;
|
||||
|
||||
private bool _dataIsValid = false;
|
||||
|
||||
public void ShowTooltipOnCursor(string text, string? title = null, uint id = 0, string name = "")
|
||||
{
|
||||
ShowTooltip(text, ImGui.GetMousePos(), title, id, name);
|
||||
}
|
||||
|
||||
public void ShowTooltip(string text, Vector2 position, string? title = null, uint id = 0, string name = "")
|
||||
{
|
||||
if (text == null)
|
||||
{
|
||||
if (_config.DebugTooltips)
|
||||
Plugin.Logger.Information("[HSUI Tooltip DBG] ShowTooltip skipped: text is null");
|
||||
return;
|
||||
}
|
||||
|
||||
// remove styling tags from text
|
||||
if (_previousRawText != text)
|
||||
{
|
||||
_currentTooltipText = text;
|
||||
_previousRawText = text;
|
||||
}
|
||||
|
||||
// calcualte title size
|
||||
_titleSize = Vector2.Zero;
|
||||
if (title != null)
|
||||
{
|
||||
_currentTooltipTitle = title;
|
||||
|
||||
if (_config.ShowSourceName && name.Length > 0)
|
||||
{
|
||||
_currentTooltipTitle += $" ({name})";
|
||||
}
|
||||
|
||||
if (_config.ShowStatusIDs)
|
||||
{
|
||||
_currentTooltipTitle += " (ID: " + id + ")";
|
||||
}
|
||||
|
||||
using (FontsManager.Instance.PushFont(FontsConfig.DefaultSmallFontKey))
|
||||
{
|
||||
_titleSize = ImGui.CalcTextSize(_currentTooltipTitle, false, MaxWidth);
|
||||
_titleSize.Y += Margin;
|
||||
}
|
||||
}
|
||||
|
||||
// calculate text size
|
||||
using (FontsManager.Instance.PushFont(FontsConfig.DefaultSmallFontKey))
|
||||
{
|
||||
_textSize = ImGui.CalcTextSize(_currentTooltipText, false, MaxWidth);
|
||||
}
|
||||
|
||||
_size = new Vector2(Math.Max(_titleSize.X, _textSize.X) + Margin * 2, _titleSize.Y + _textSize.Y + Margin * 2);
|
||||
|
||||
// position tooltip using the given coordinates as bottom center
|
||||
position.X = position.X - _size.X / 2f;
|
||||
position.Y = position.Y - _size.Y;
|
||||
|
||||
// correct tooltips off screen
|
||||
_position = ConstrainPosition(position, _size);
|
||||
|
||||
_dataIsValid = true;
|
||||
if (_config.DebugTooltips)
|
||||
Plugin.Logger.Information($"[HSUI Tooltip DBG] ShowTooltip: title='{title}' textLen={text?.Length ?? 0} textPreview='{(text != null && text.Length > 80 ? text[..80] + "..." : text ?? "")}' pos=({_position.X:F0},{_position.Y:F0})");
|
||||
}
|
||||
|
||||
public void RemoveTooltip()
|
||||
{
|
||||
_dataIsValid = false;
|
||||
}
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
if (!_dataIsValid)
|
||||
return;
|
||||
if (ConfigurationManager.Instance.ShowingModalWindow)
|
||||
{
|
||||
if (_config.DebugTooltips)
|
||||
Plugin.Logger.Information("[HSUI Tooltip DBG] Draw SKIPPED: ShowingModalWindow=true");
|
||||
return;
|
||||
}
|
||||
|
||||
// bg
|
||||
ImGuiWindowFlags windowFlags =
|
||||
ImGuiWindowFlags.NoTitleBar
|
||||
| ImGuiWindowFlags.NoMove
|
||||
| ImGuiWindowFlags.NoDecoration
|
||||
| ImGuiWindowFlags.NoBackground
|
||||
| ImGuiWindowFlags.NoInputs
|
||||
| ImGuiWindowFlags.NoSavedSettings
|
||||
| ImGuiWindowFlags.NoFocusOnAppearing;
|
||||
|
||||
// imgui clips the left and right borders inside windows for some reason
|
||||
// we make the window bigger so the actual drawable size is the expected one
|
||||
var windowMargin = new Vector2(4, 0);
|
||||
var windowPos = _position - windowMargin;
|
||||
|
||||
ImGui.SetNextWindowPos(windowPos, ImGuiCond.Always);
|
||||
ImGui.SetNextWindowSize(_size + windowMargin * 2);
|
||||
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0);
|
||||
ImGui.Begin("DelvUI_tooltip", windowFlags);
|
||||
var drawList = ImGui.GetWindowDrawList();
|
||||
|
||||
drawList.AddRectFilled(_position, _position + _size, _config.BackgroundColor.Base);
|
||||
|
||||
if (_config.BorderConfig.Enabled)
|
||||
{
|
||||
drawList.AddRect(_position, _position + _size, _config.BorderConfig.Color.Base, 0, ImDrawFlags.None, _config.BorderConfig.Thickness);
|
||||
}
|
||||
|
||||
// no idea why i have to do this
|
||||
float globalScaleCorrection = -15 + 15 * ImGuiHelpers.GlobalScale;
|
||||
|
||||
if (_currentTooltipTitle != null)
|
||||
{
|
||||
// title
|
||||
Vector2 cursorPos;
|
||||
using (FontsManager.Instance.PushFont(FontsConfig.DefaultSmallFontKey))
|
||||
{
|
||||
cursorPos = new Vector2(windowMargin.X + _size.X / 2f - _titleSize.X / 2f, Margin);
|
||||
ImGui.SetCursorPos(cursorPos);
|
||||
ImGui.PushTextWrapPos(cursorPos.X + _titleSize.X + globalScaleCorrection + Margin);
|
||||
ImGui.TextColored(_config.TitleColor.Vector, _currentTooltipTitle);
|
||||
ImGui.PopTextWrapPos();
|
||||
}
|
||||
|
||||
// text
|
||||
using (FontsManager.Instance.PushFont(FontsConfig.DefaultSmallFontKey))
|
||||
{
|
||||
cursorPos = new Vector2(windowMargin.X + _size.X / 2f - _textSize.X / 2f, Margin + _titleSize.Y);
|
||||
ImGui.SetCursorPos(cursorPos);
|
||||
ImGui.PushTextWrapPos(cursorPos.X + _textSize.X + globalScaleCorrection + Margin);
|
||||
ImGui.TextColored(_config.TextColor.Vector, _currentTooltipText);
|
||||
ImGui.PopTextWrapPos();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// text
|
||||
using (FontsManager.Instance.PushFont(FontsConfig.DefaultSmallFontKey))
|
||||
{
|
||||
var cursorPos = windowMargin + new Vector2(Margin, Margin);
|
||||
var textWidth = _size.X - Margin * 2;
|
||||
|
||||
ImGui.SetCursorPos(cursorPos);
|
||||
ImGui.PushTextWrapPos(cursorPos.X + textWidth + globalScaleCorrection + Margin);
|
||||
ImGui.TextColored(_config.TextColor.Vector, _currentTooltipText);
|
||||
ImGui.PopTextWrapPos();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.End();
|
||||
ImGui.PopStyleVar();
|
||||
|
||||
if (_config.DebugTooltips)
|
||||
Plugin.Logger.Information($"[HSUI Tooltip DBG] Draw rendered tooltip at ({_position.X:F0},{_position.Y:F0})");
|
||||
RemoveTooltip();
|
||||
}
|
||||
|
||||
private Vector2 ConstrainPosition(Vector2 position, Vector2 size)
|
||||
{
|
||||
var screenSize = ImGui.GetWindowViewport().Size;
|
||||
|
||||
if (position.X < 0)
|
||||
{
|
||||
position.X = Margin;
|
||||
}
|
||||
else if (position.X + size.X > screenSize.X)
|
||||
{
|
||||
position.X = screenSize.X - size.X - Margin;
|
||||
}
|
||||
|
||||
if (position.Y < 0)
|
||||
{
|
||||
position.Y = Margin;
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
}
|
||||
|
||||
[Section("Misc")]
|
||||
[SubSection("Tooltips", 0)]
|
||||
public class TooltipsConfig : PluginConfigObject
|
||||
{
|
||||
public new static TooltipsConfig DefaultConfig() { return new TooltipsConfig(); }
|
||||
|
||||
[Checkbox("Debug Tooltips")]
|
||||
[Order(3)]
|
||||
public bool DebugTooltips = false;
|
||||
|
||||
[Checkbox("Show Status Effects IDs")]
|
||||
[Order(5)]
|
||||
public bool ShowStatusIDs = false;
|
||||
|
||||
[Checkbox("Show Source Name")]
|
||||
[Order(10)]
|
||||
public bool ShowSourceName = false;
|
||||
|
||||
[ColorEdit4("Background Color")]
|
||||
[Order(15)]
|
||||
public PluginConfigColor BackgroundColor = new PluginConfigColor(new(19f / 255f, 19f / 255f, 19f / 255f, 190f / 250f));
|
||||
|
||||
[ColorEdit4("Title Color")]
|
||||
[Order(20)]
|
||||
public PluginConfigColor TitleColor = new PluginConfigColor(new(255f / 255f, 210f / 255f, 31f / 255f, 100f / 100f));
|
||||
|
||||
[ColorEdit4("Text Color")]
|
||||
[Order(35)]
|
||||
public PluginConfigColor TextColor = new PluginConfigColor(new(255f / 255f, 255f / 255f, 255f / 255f, 100f / 100f));
|
||||
|
||||
[NestedConfig("Border", 40, separator = false, spacing = true, collapsingHeader = false)]
|
||||
public TooltipBorderConfig BorderConfig = new();
|
||||
}
|
||||
|
||||
[Exportable(false)]
|
||||
public class TooltipBorderConfig : PluginConfigObject
|
||||
{
|
||||
[ColorEdit4("Color")]
|
||||
[Order(5)]
|
||||
public PluginConfigColor Color = new(new Vector4(10f / 255f, 10f / 255f, 10f / 255f, 160f / 255f));
|
||||
|
||||
[DragInt("Thickness", min = 1, max = 100)]
|
||||
[Order(10)]
|
||||
public int Thickness = 4;
|
||||
|
||||
public TooltipBorderConfig()
|
||||
{
|
||||
}
|
||||
|
||||
public TooltipBorderConfig(PluginConfigColor color, int thickness)
|
||||
{
|
||||
Color = color;
|
||||
Thickness = thickness;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Game.ClientState.Statuses;
|
||||
using Dalamud.Plugin.Services;
|
||||
using HSUI.Config;
|
||||
using HSUI.Enums;
|
||||
using HSUI.Interface.GeneralElements;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using StructsCharacter = FFXIVClientStructs.FFXIV.Client.Game.Character.Character;
|
||||
using StructsCharacterManager = FFXIVClientStructs.FFXIV.Client.Game.Character.CharacterManager;
|
||||
using StructsGameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
|
||||
|
||||
namespace HSUI.Helpers
|
||||
{
|
||||
internal static class Utils
|
||||
{
|
||||
private static uint InvalidGameObjectId = 0xE0000000;
|
||||
|
||||
public static IGameObject? GetBattleChocobo(IGameObject? player)
|
||||
{
|
||||
if (player == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return GetBuddy(player.GameObjectId, BattleNpcSubKind.Chocobo);
|
||||
}
|
||||
|
||||
public static IGameObject? GetBuddy(ulong ownerId, BattleNpcSubKind kind)
|
||||
{
|
||||
// only the first 200 elements in the array are relevant due to the order in which SE packs data into the array
|
||||
// we do a step of 2 because its always an actor followed by its companion
|
||||
for (var i = 0; i < 200; i += 2)
|
||||
{
|
||||
var gameObject = Plugin.ObjectTable[i];
|
||||
|
||||
if (gameObject == null || gameObject.GameObjectId == InvalidGameObjectId || gameObject is not IBattleNpc battleNpc)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (battleNpc.BattleNpcKind == kind && battleNpc.OwnerId == ownerId)
|
||||
{
|
||||
return gameObject;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static IGameObject? GetGameObjectByName(string name)
|
||||
{
|
||||
// only the first 200 elements in the array are relevant due to the order in which SE packs data into the array
|
||||
// we do a step of 2 because its always an actor followed by its companion
|
||||
for (int i = 0; i < 200; i += 2)
|
||||
{
|
||||
IGameObject? gameObject = Plugin.ObjectTable[i];
|
||||
|
||||
if (gameObject == null || gameObject.GameObjectId == InvalidGameObjectId || gameObject.GameObjectId == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (gameObject.Name.ToString() == name)
|
||||
{
|
||||
return gameObject;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static unsafe bool IsHostile(IGameObject obj)
|
||||
{
|
||||
StructsGameObject* gameObject = (StructsGameObject*)obj.Address;
|
||||
byte plateType = gameObject->GetNamePlateColorType();
|
||||
|
||||
// 4, 5, 6: Enemy players in PvP
|
||||
// 7: yellow, can be attacked, not engaged
|
||||
// 8: dead
|
||||
// 9: red, engaged with your party
|
||||
// 10: purple, engaged with other party
|
||||
// 11: orange, aggro'd to your party but not attacked yet
|
||||
return plateType >= 4 && plateType <= 11;
|
||||
}
|
||||
|
||||
public static unsafe float ActorShieldValue(IGameObject? actor)
|
||||
{
|
||||
if (actor == null || actor is not ICharacter)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
StructsCharacter* chara = (StructsCharacter*)actor.Address;
|
||||
return Math.Min(chara->CharacterData.ShieldValue, 100f) / 100f;
|
||||
}
|
||||
|
||||
public static bool IsActorCasting(IGameObject? actor)
|
||||
{
|
||||
if (actor is not IBattleChara chara)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return chara.IsCasting;
|
||||
}
|
||||
catch { }
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static IEnumerable<IStatus> StatusListForActor(IGameObject? obj)
|
||||
{
|
||||
if (obj is IBattleChara chara)
|
||||
{
|
||||
return StatusListForBattleChara(chara);
|
||||
}
|
||||
|
||||
return new List<IStatus>();
|
||||
}
|
||||
|
||||
public static IEnumerable<IStatus> StatusListForBattleChara(IBattleChara? chara)
|
||||
{
|
||||
List<IStatus> statusList = new List<IStatus>();
|
||||
if (chara == null)
|
||||
{
|
||||
return statusList;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
statusList = chara.StatusList.ToList();
|
||||
}
|
||||
catch { }
|
||||
|
||||
return statusList;
|
||||
}
|
||||
|
||||
public static string DurationToString(double duration, int decimalCount = 0)
|
||||
{
|
||||
if (duration == 0)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
TimeSpan t = TimeSpan.FromSeconds(duration);
|
||||
|
||||
if (t.Hours >= 1) { return t.Hours + "h"; }
|
||||
if (t.Minutes >= 5) { return t.Minutes + "m"; }
|
||||
if (t.Minutes >= 1) { return $"{t.Minutes}:{t.Seconds:00}"; }
|
||||
|
||||
return duration.ToString("N" + decimalCount, ConfigurationManager.Instance.ActiveCultreInfo);
|
||||
}
|
||||
|
||||
public static IStatus? GetTankInvulnerabilityID(IBattleChara actor)
|
||||
{
|
||||
return StatusListForBattleChara(actor).FirstOrDefault(o => o.StatusId is 810 or 811 or 3255 or 1302 or 409 or 1836 or 82);
|
||||
}
|
||||
|
||||
public static bool IsOnCleanseJob()
|
||||
{
|
||||
IPlayerCharacter? player = Plugin.ObjectTable.LocalPlayer;
|
||||
|
||||
return player != null && JobsHelper.IsJobWithCleanse(player.ClassJob.RowId, player.Level);
|
||||
}
|
||||
|
||||
public static IGameObject? FindTargetOfTarget(IGameObject? target, IGameObject? player, IObjectTable actors)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Dalamud for now has an issue where it is only able to get the target ID of
|
||||
// NON-Networked objects through anything but GetTargetId on ClientStruct Gameobjects.
|
||||
// The bypass converts all Dalamud GameObject Data to ClientStructs GameObject Data and handles it accordingly.
|
||||
int actualTargetId = GetActualTargetId(target);
|
||||
// The Object ID that gets returned from minions is in reality the index
|
||||
// Checking for the correct object ID wouldn't work anyways as you would yet again run into the ObjectID = 0xE0000000 issue
|
||||
if (actualTargetId >= 0 && actualTargetId < actors.Length)
|
||||
{
|
||||
return actors[actualTargetId];
|
||||
}
|
||||
|
||||
if (target.TargetObjectId == 0 && player != null && player.TargetObjectId == 0)
|
||||
{
|
||||
return player;
|
||||
}
|
||||
|
||||
// only the first 200 elements in the array are relevant due to the order in which SE packs data into the array
|
||||
// we do a step of 2 because its always an actor followed by its companion
|
||||
for (int i = 0; i < 200; i += 2)
|
||||
{
|
||||
IGameObject? actor = actors[i];
|
||||
if (actor?.GameObjectId == target.TargetObjectId)
|
||||
{
|
||||
return actor;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the actual target ID of your targets target.
|
||||
/// </summary>
|
||||
/// <param name="target">Your target</param>
|
||||
/// <returns>Target ID of your targets targer. Returns -1 if old code should be ran.</returns>
|
||||
private static unsafe int GetActualTargetId(IGameObject target)
|
||||
{
|
||||
// We only need to check for companions.
|
||||
// Why not check target.TargetObject?.ObjectKind == ObjectKind.Companion?
|
||||
// Due to the Non-Networked game object bug the game is unaware of what type the object should actually be
|
||||
if (target.TargetObject?.ObjectKind != ObjectKind.Player)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Here we get the ClientStruct Character of our target (aka the player we are targeting)
|
||||
StructsCharacter targetChara = StructsCharacterManager.Instance()->LookupBattleCharaByEntityId(target.EntityId)->Character;
|
||||
|
||||
// This method is key. GetTargetId() returns the targets player target ID. If it is converted to a hex string and starts with the number 4, it is a minion.
|
||||
// Even though it is a minion, it still returns the players target ID.
|
||||
ulong realTargetID = targetChara.GetTargetId();
|
||||
if (!realTargetID.ToString("X").StartsWith("4"))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
// We look up the parents ClientStruct GameObject
|
||||
StructsCharacter* realBattleChara = (StructsCharacter*)StructsCharacterManager.Instance()->LookupBattleCharaByEntityId((uint)realTargetID);
|
||||
if (realBattleChara == null)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
// And get the companion off of that
|
||||
StructsGameObject* companionGameObject = (StructsGameObject*)realBattleChara->CompanionData.CompanionObject;
|
||||
if (companionGameObject == null)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
// We return the index of the object here. Why?
|
||||
// Again due to the bug where ObjectID = 0xE0000000
|
||||
// The index does work and returns the exact minion index.
|
||||
return companionGameObject->ObjectIndex;
|
||||
}
|
||||
|
||||
public static Vector2 GetAnchoredPosition(Vector2 position, Vector2 size, DrawAnchor anchor)
|
||||
{
|
||||
return anchor switch
|
||||
{
|
||||
DrawAnchor.Center => position - size / 2f,
|
||||
DrawAnchor.Left => position + new Vector2(0, -size.Y / 2f),
|
||||
DrawAnchor.Right => position + new Vector2(-size.X, -size.Y / 2f),
|
||||
DrawAnchor.Top => position + new Vector2(-size.X / 2f, 0),
|
||||
DrawAnchor.TopLeft => position,
|
||||
DrawAnchor.TopRight => position + new Vector2(-size.X, 0),
|
||||
DrawAnchor.Bottom => position + new Vector2(-size.X / 2f, -size.Y),
|
||||
DrawAnchor.BottomLeft => position + new Vector2(0, -size.Y),
|
||||
DrawAnchor.BottomRight => position + new Vector2(-size.X, -size.Y),
|
||||
_ => position
|
||||
};
|
||||
}
|
||||
|
||||
public static string UserFriendlyConfigName(string configTypeName) => UserFriendlyString(configTypeName, "Config");
|
||||
|
||||
public static string UserFriendlyString(string str, string? remove)
|
||||
{
|
||||
string? s = remove != null ? str.Replace(remove, "") : str;
|
||||
|
||||
Regex? regex = new(@"
|
||||
(?<=[A-Z])(?=[A-Z][a-z]) |
|
||||
(?<=[^A-Z])(?=[A-Z]) |
|
||||
(?<=[A-Za-z])(?=[^A-Za-z])",
|
||||
RegexOptions.IgnorePatternWhitespace);
|
||||
|
||||
return regex.Replace(s, " ");
|
||||
}
|
||||
|
||||
public static void OpenUrl(string url)
|
||||
{
|
||||
try
|
||||
{
|
||||
Process.Start(url);
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
// hack because of this: https://github.com/dotnet/corefx/issues/10361
|
||||
if (RuntimeInformation.IsOSPlatform(osPlatform: OSPlatform.Windows))
|
||||
{
|
||||
url = url.Replace("&", "^&");
|
||||
Process.Start(new ProcessStartInfo("cmd", $"/c start {url}") { CreateNoWindow = true });
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
Process.Start("xdg-open", url);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Plugin.Logger.Error("Error trying to open url: " + e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static unsafe bool? IsTargetCasting()
|
||||
{
|
||||
AtkUnitBase* addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("_TargetInfo", 1).Address;
|
||||
if (addon != null && addon->IsVisible)
|
||||
{
|
||||
AtkImageNode* imageNode = addon->GetImageNodeById(15);
|
||||
return imageNode == null || imageNode->IsVisible();
|
||||
}
|
||||
|
||||
addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("_TargetInfoCastBar", 1).Address;
|
||||
if (addon != null && addon->IsVisible)
|
||||
{
|
||||
AtkImageNode* imageNode = addon->GetImageNodeById(7);
|
||||
return imageNode != null || imageNode->IsVisible();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static unsafe bool? IsFocusTargetCasting()
|
||||
{
|
||||
AtkUnitBase* addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("_FocusTargetInfo", 1).Address;
|
||||
if (addon != null && addon->IsVisible)
|
||||
{
|
||||
AtkTextNode* textNode = addon->GetTextNodeById(5);
|
||||
return textNode == null || textNode->IsVisible();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static unsafe bool? IsEnemyInListCasting(int index)
|
||||
{
|
||||
if (index < 0 || index > 7) { return null; }
|
||||
|
||||
AtkUnitBase* addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("_EnemyList", 1).Address;
|
||||
if (addon == null || !addon->IsVisible) { return null; }
|
||||
|
||||
uint buttonId = (index == 0) ? 2u : (uint)(20000 + index);
|
||||
AtkComponentButton* button = addon->GetComponentButtonById(buttonId);
|
||||
if (button == null || button->AtkResNode == null || !button->AtkResNode->IsVisible())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
AtkImageNode* imageNode = button->GetImageNodeById(8);
|
||||
return imageNode == null || imageNode->IsVisible();
|
||||
}
|
||||
|
||||
public static unsafe uint? SignIconIDForActor(IGameObject? actor)
|
||||
{
|
||||
if (actor == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return SignIconIDForObjectID(actor.GameObjectId);
|
||||
}
|
||||
|
||||
public static unsafe uint? SignIconIDForObjectID(ulong objectId)
|
||||
{
|
||||
MarkingController* markingController = MarkingController.Instance();
|
||||
if (objectId == 0 || objectId == InvalidGameObjectId || markingController == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
for (int i = 0; i < 17; i++)
|
||||
{
|
||||
if (objectId == markingController->Markers[i])
|
||||
{
|
||||
// attack1-5
|
||||
if (i <= 4)
|
||||
{
|
||||
return (uint)(61201 + i);
|
||||
}
|
||||
// attack6-8
|
||||
else if (i >= 14)
|
||||
{
|
||||
return (uint)(61201 + i - 9);
|
||||
}
|
||||
// shapes
|
||||
else if (i >= 10)
|
||||
{
|
||||
return (uint)(61231 + i - 10);
|
||||
}
|
||||
// ignore1-2
|
||||
else if (i >= 8)
|
||||
{
|
||||
return (uint)(61221 + i - 8);
|
||||
}
|
||||
// bind1-3
|
||||
else if (i >= 5)
|
||||
{
|
||||
return (uint)(61211 + i - 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static bool IsHealthLabel(LabelConfig config)
|
||||
{
|
||||
return config.GetText().Contains("[health");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
using Dalamud.Game.ClientState.Party;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Interface.Textures.TextureWraps;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using HSUI.Config;
|
||||
using HSUI.Config.Tree;
|
||||
using HSUI.Interface.Party;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.IO;
|
||||
using static System.Collections.Specialized.BitVector32;
|
||||
|
||||
namespace HSUI.Helpers
|
||||
{
|
||||
public enum WhosTalkingState : int
|
||||
{
|
||||
None = 0,
|
||||
Speaking = 1,
|
||||
Muted = 2,
|
||||
Deafened = 3
|
||||
}
|
||||
|
||||
public class WhosTalkingHelper
|
||||
{
|
||||
private readonly ICallGateSubscriber<string, int> _getUserState;
|
||||
private Dictionary<string, WhosTalkingState> _cachedStates = new Dictionary<string, WhosTalkingState>();
|
||||
|
||||
private string speakingPath = "";
|
||||
private string mutedPath = "";
|
||||
private string deafenedPath = "";
|
||||
|
||||
#region Singleton
|
||||
private WhosTalkingHelper()
|
||||
{
|
||||
_getUserState = Plugin.PluginInterface.GetIpcSubscriber<string, int>("WT.GetUserState");
|
||||
|
||||
try
|
||||
{
|
||||
string imagesPath = Path.Combine(Plugin.AssemblyLocation, "Media", "Images");
|
||||
|
||||
// speaking
|
||||
speakingPath = Path.Combine(imagesPath, "speaking.png");
|
||||
|
||||
// muted
|
||||
mutedPath = Path.Combine(imagesPath, "muted.png");
|
||||
|
||||
// deafened
|
||||
deafenedPath = Path.Combine(imagesPath, "deafened.png");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public static void Initialize() { Instance = new WhosTalkingHelper(); }
|
||||
|
||||
public static WhosTalkingHelper Instance { get; private set; } = null!;
|
||||
|
||||
~WhosTalkingHelper()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Instance = null!;
|
||||
}
|
||||
#endregion
|
||||
|
||||
public void Update()
|
||||
{
|
||||
_cachedStates.Clear();
|
||||
|
||||
foreach (IPartyFramesMember member in PartyManager.Instance.GroupMembers)
|
||||
{
|
||||
if (member.Name.Length <= 0) { continue; }
|
||||
|
||||
WhosTalkingState state = WhosTalkingState.None;
|
||||
|
||||
try
|
||||
{
|
||||
state = (WhosTalkingState)_getUserState.InvokeFunc(member.Name);
|
||||
}
|
||||
catch { }
|
||||
|
||||
if (!_cachedStates.ContainsKey(member.Name))
|
||||
{
|
||||
_cachedStates.Add(member.Name, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public WhosTalkingState GetUserState(string name)
|
||||
{
|
||||
if (_cachedStates.TryGetValue(name, out WhosTalkingState state))
|
||||
{
|
||||
return state;
|
||||
}
|
||||
|
||||
return WhosTalkingState.None;
|
||||
}
|
||||
|
||||
public IDalamudTextureWrap? GetTextureForState(WhosTalkingState state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case WhosTalkingState.Speaking: return Plugin.TextureProvider.GetFromFile(speakingPath).GetWrapOrDefault();
|
||||
case WhosTalkingState.Muted: return Plugin.TextureProvider.GetFromFile(mutedPath).GetWrapOrDefault();
|
||||
case WhosTalkingState.Deafened: return Plugin.TextureProvider.GetFromFile(deafenedPath).GetWrapOrDefault();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using HSUI.Config;
|
||||
using HSUI.Config.Tree;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace HSUI.Helpers
|
||||
{
|
||||
internal class WotsitHelper
|
||||
{
|
||||
private readonly ICallGateSubscriber<string, string, string, uint, string> _registerWithSearch;
|
||||
private readonly ICallGateSubscriber<string, bool> _invoke;
|
||||
private readonly ICallGateSubscriber<string, bool> _unregisterAll;
|
||||
|
||||
private Dictionary<string, (SectionNode, SubSectionNode?, NestedSubSectionNode?)> _map = new Dictionary<string, (SectionNode, SubSectionNode?, NestedSubSectionNode?)>();
|
||||
|
||||
#region Singleton
|
||||
private WotsitHelper()
|
||||
{
|
||||
_registerWithSearch = Plugin.PluginInterface.GetIpcSubscriber<string, string, string, uint, string>("FA.RegisterWithSearch");
|
||||
_unregisterAll = Plugin.PluginInterface.GetIpcSubscriber<string, bool>("FA.UnregisterAll");
|
||||
|
||||
_invoke = Plugin.PluginInterface.GetIpcSubscriber<string, bool>("FA.Invoke");
|
||||
_invoke.Subscribe(Invoke);
|
||||
}
|
||||
|
||||
public static void Initialize() { Instance = new WotsitHelper(); }
|
||||
|
||||
public static WotsitHelper Instance { get; private set; } = null!;
|
||||
|
||||
~WotsitHelper()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
|
||||
UnregisterAll();
|
||||
}
|
||||
|
||||
protected void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Instance = null!;
|
||||
}
|
||||
#endregion
|
||||
|
||||
public void Update()
|
||||
{
|
||||
_map.Clear();
|
||||
if (!UnregisterAll())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// sections
|
||||
foreach (Node node in ConfigurationManager.Instance.ConfigBaseNode.Sections)
|
||||
{
|
||||
if (node is not SectionNode section) { continue; }
|
||||
|
||||
string guid = _registerWithSearch.InvokeFunc(
|
||||
Plugin.PluginInterface.InternalName,
|
||||
"HSUI Settings: " + section.Name,
|
||||
"HSUI " + section.Name,
|
||||
66472
|
||||
);
|
||||
|
||||
_map.Add(guid, (section, null, null));
|
||||
|
||||
// sub sections
|
||||
foreach (SubSectionNode subSection in section.Children)
|
||||
{
|
||||
guid = _registerWithSearch.InvokeFunc(
|
||||
Plugin.PluginInterface.InternalName,
|
||||
"HSUI Settings: " + section.Name + " > " + subSection.Name,
|
||||
"HSUI " + subSection.Name,
|
||||
66472
|
||||
);
|
||||
|
||||
_map.Add(guid, (section, subSection, null));
|
||||
|
||||
// nested sub sections
|
||||
foreach (SubSectionNode nestedSubSection in subSection.Children)
|
||||
{
|
||||
if (nestedSubSection is not NestedSubSectionNode nestedNode) { continue; }
|
||||
|
||||
guid = _registerWithSearch.InvokeFunc(
|
||||
Plugin.PluginInterface.InternalName,
|
||||
"HSUI Settings: " + section.Name + " > " + subSection.Name + " > " + nestedNode.Name,
|
||||
"HSUI " + nestedNode.Name,
|
||||
66472
|
||||
);
|
||||
|
||||
_map.Add(guid, (section, subSection, nestedNode));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Invoke(string guid)
|
||||
{
|
||||
//_map.TryGetValue()
|
||||
if (_map.TryGetValue(guid, out var value) && value.Item1 != null)
|
||||
{
|
||||
SectionNode section = value.Item1;
|
||||
ConfigurationManager.Instance.ConfigBaseNode.SelectedOptionName = section.Name;
|
||||
ConfigurationManager.Instance.ConfigBaseNode.RefreshSelectedNode();
|
||||
|
||||
SubSectionNode? subSectionNode = value.Item2;
|
||||
if (subSectionNode != null)
|
||||
{
|
||||
section.ForceSelectedTabName = subSectionNode.Name;
|
||||
|
||||
NestedSubSectionNode? nestedSubSectionNode = value.Item3;
|
||||
if (nestedSubSectionNode != null)
|
||||
{
|
||||
subSectionNode.ForceSelectedTabName = nestedSubSectionNode.Name;
|
||||
}
|
||||
}
|
||||
|
||||
ConfigurationManager.Instance.OpenConfigWindow();
|
||||
}
|
||||
}
|
||||
|
||||
public bool UnregisterAll()
|
||||
{
|
||||
try
|
||||
{
|
||||
_unregisterAll.InvokeFunc(Plugin.PluginInterface.InternalName);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user