Initial release: HSUI v1.0.0.0 - HUD replacement with configurable hotbars

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-01-30 23:52:46 -05:00
commit f37369cdda
202 changed files with 40137 additions and 0 deletions
+78
View File
@@ -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);
}
}
}
+457
View File
@@ -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 110, slotIndex 011. 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;
}
}
}
+197
View File
@@ -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", "");
}
}
}
+440
View File
@@ -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)));
}
}
}
+312
View File
@@ -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;
}
}
}
+257
View File
@@ -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)
};
}
}
}
+384
View File
@@ -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();
}
}
}
}
}
+43
View File
@@ -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;
}
}
+115
View File
@@ -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;
}
}
}
}
+233
View File
@@ -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();
}
}
}
+86
View File
@@ -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;
}
}
}
+76
View File
@@ -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;
}
}
}
+69
View File
@@ -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}");
}
}
}
}
+286
View File
@@ -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;
}
}
}
+621
View File
@@ -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
}
}
+710
View File
@@ -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;
}
}
+156
View File
@@ -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;
}
}
}
+322
View File
@@ -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,
}
}
+98
View File
@@ -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);
}
}
}
+85
View File
@@ -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);
}
}
}
+234
View File
@@ -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;
}
}
+48
View File
@@ -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;
}
}
}
+76
View File
@@ -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);
}
}
+531
View File
@@ -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() : "-";
}
}
}
+34
View File
@@ -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();
}
}
}
+296
View File
@@ -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;
}
}
}
+429
View File
@@ -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");
}
}
}
+126
View File
@@ -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;
}
}
}
+145
View File
@@ -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;
}
}
}
}