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>(_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(); } public bool Enabled => _config.Enabled; public WindowClippingMode? Mode => _config.Enabled ? _config.Mode : null; private List _clipRects = new List(); private List _extraClipRects = new List(); private static Dictionary _thirdPartyClipRects = new(); private static string _sharedDataId = "HSUI.ClipRects"; private static List _ignoredAddonNames = new List() { "_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 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 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 GetHotbarsClipRects() { List rects = new List(); 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 GetNPCChatBubbleClipRect() { List rects = new List(); 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 GetPlayerChatBubbleClipRect() { List rects = new List(); 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 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 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))); } } }