Initial release: HSUI v1.0.0.0 - HUD replacement with configurable hotbars
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user