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
@@ -0,0 +1,34 @@
using System.Collections.Generic;
using System.Linq;
using Dalamud.Game.ClientState.Objects.Types;
namespace HSUI.Interface.StatusEffects
{
public class CustomEffectsListHud : StatusEffectsListHud
{
public CustomEffectsListHud(StatusEffectsListConfig config, string displayName) : base(config, displayName)
{
}
public IGameObject? TargetActor { get; set; } = null!;
protected override List<StatusEffectData> StatusEffectsData()
{
var list = StatusEffectDataList(TargetActor);
list.AddRange(StatusEffectDataList(Actor));
// cull duplicate statuses from the same source
list = list.GroupBy(s => new { s.Status.StatusId, s.Status.SourceObject.Id })
.Select(status => status.First())
.ToList();
// show mine or permanent first
if (Config.ShowMineFirst || Config.ShowPermanentFirst)
{
return OrderByMineOrPermanentFirst(list);
}
return list;
}
}
}
@@ -0,0 +1,859 @@
using Dalamud.Interface;
using HSUI.Config;
using HSUI.Config.Attributes;
using HSUI.Enums;
using HSUI.Helpers;
using HSUI.Interface.Bars;
using HSUI.Interface.GeneralElements;
using Dalamud.Bindings.ImGui;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
namespace HSUI.Interface.StatusEffects
{
[Section("Buffs and Debuffs")]
[SubSection("Player Buffs", 0)]
public class PlayerBuffsListConfig : UnitFrameStatusEffectsListConfig
{
public new static PlayerBuffsListConfig DefaultConfig()
{
var screenSize = ImGui.GetMainViewport().Size;
var pos = new Vector2(screenSize.X * 0.38f, -screenSize.Y * 0.45f);
var iconConfig = new StatusEffectIconConfig();
iconConfig.DispellableBorderConfig.Enabled = false;
return new PlayerBuffsListConfig(pos, HUDConstants.DefaultStatusEffectsListSize, true, false, true, GrowthDirections.Left | GrowthDirections.Down, iconConfig);
}
public PlayerBuffsListConfig(Vector2 position, Vector2 size, bool showBuffs, bool showDebuffs, bool showPermanentEffects,
GrowthDirections growthDirections, StatusEffectIconConfig iconConfig)
: base(position, size, showBuffs, showDebuffs, showPermanentEffects, growthDirections, iconConfig)
{
}
}
[Section("Buffs and Debuffs")]
[SubSection("Player Debuffs", 0)]
public class PlayerDebuffsListConfig : UnitFrameStatusEffectsListConfig
{
public new static PlayerDebuffsListConfig DefaultConfig()
{
var screenSize = ImGui.GetMainViewport().Size;
var pos = new Vector2(screenSize.X * 0.38f, -screenSize.Y * 0.45f + HUDConstants.DefaultStatusEffectsListSize.Y);
var iconConfig = new StatusEffectIconConfig();
return new PlayerDebuffsListConfig(pos, HUDConstants.DefaultStatusEffectsListSize, false, true, true, GrowthDirections.Left | GrowthDirections.Down, iconConfig);
}
public PlayerDebuffsListConfig(Vector2 position, Vector2 size, bool showBuffs, bool showDebuffs, bool showPermanentEffects,
GrowthDirections growthDirections, StatusEffectIconConfig iconConfig)
: base(position, size, showBuffs, showDebuffs, showPermanentEffects, growthDirections, iconConfig)
{
}
}
[Section("Buffs and Debuffs")]
[SubSection("Target Buffs", 0)]
public class TargetBuffsListConfig : UnitFrameStatusEffectsListConfig
{
public new static TargetBuffsListConfig DefaultConfig()
{
var pos = new Vector2(0, -1);
var iconConfig = new StatusEffectIconConfig();
iconConfig.DispellableBorderConfig.Enabled = false;
var config = new TargetBuffsListConfig(pos, HUDConstants.DefaultStatusEffectsListSize, true, false, true, GrowthDirections.Right | GrowthDirections.Up, iconConfig);
config.AnchorToUnitFrame = true;
config.UnitFrameAnchor = DrawAnchor.TopLeft;
return config;
}
public TargetBuffsListConfig(Vector2 position, Vector2 size, bool showBuffs, bool showDebuffs, bool showPermanentEffects,
GrowthDirections growthDirections, StatusEffectIconConfig iconConfig)
: base(position, size, showBuffs, showDebuffs, showPermanentEffects, growthDirections, iconConfig)
{
}
}
[Section("Buffs and Debuffs")]
[SubSection("Target Debuffs", 0)]
public class TargetDebuffsListConfig : UnitFrameStatusEffectsListConfig
{
public new static TargetDebuffsListConfig DefaultConfig()
{
var pos = new Vector2(0, -85);
var iconConfig = new StatusEffectIconConfig();
iconConfig.DispellableBorderConfig.Enabled = false;
var config = new TargetDebuffsListConfig(pos, HUDConstants.DefaultStatusEffectsListSize, false, true, true, GrowthDirections.Right | GrowthDirections.Up, iconConfig);
config.AnchorToUnitFrame = true;
config.UnitFrameAnchor = DrawAnchor.TopLeft;
return config;
}
public TargetDebuffsListConfig(Vector2 position, Vector2 size, bool showBuffs, bool showDebuffs, bool showPermanentEffects,
GrowthDirections growthDirections, StatusEffectIconConfig iconConfig)
: base(position, size, showBuffs, showDebuffs, showPermanentEffects, growthDirections, iconConfig)
{
}
}
[Section("Buffs and Debuffs")]
[SubSection("Focus Target Buffs", 0)]
public class FocusTargetBuffsListConfig : UnitFrameStatusEffectsListConfig
{
public new static FocusTargetBuffsListConfig DefaultConfig()
{
var pos = new Vector2(0, -1);
var iconConfig = new StatusEffectIconConfig();
iconConfig.DispellableBorderConfig.Enabled = false;
var config = new FocusTargetBuffsListConfig(pos, HUDConstants.DefaultStatusEffectsListSize, true, false, true, GrowthDirections.Right | GrowthDirections.Up, iconConfig);
config.AnchorToUnitFrame = true;
config.UnitFrameAnchor = DrawAnchor.TopLeft;
return config;
}
public FocusTargetBuffsListConfig(Vector2 position, Vector2 size, bool showBuffs, bool showDebuffs, bool showPermanentEffects,
GrowthDirections growthDirections, StatusEffectIconConfig iconConfig)
: base(position, size, showBuffs, showDebuffs, showPermanentEffects, growthDirections, iconConfig)
{
}
}
[Section("Buffs and Debuffs")]
[SubSection("Focus Target Debuffs", 0)]
public class FocusTargetDebuffsListConfig : UnitFrameStatusEffectsListConfig
{
public new static FocusTargetDebuffsListConfig DefaultConfig()
{
var pos = new Vector2(0, -85);
var iconConfig = new StatusEffectIconConfig();
iconConfig.DispellableBorderConfig.Enabled = false;
var config = new FocusTargetDebuffsListConfig(pos, HUDConstants.DefaultStatusEffectsListSize, false, true, true, GrowthDirections.Right | GrowthDirections.Up, iconConfig);
config.AnchorToUnitFrame = true;
config.UnitFrameAnchor = DrawAnchor.TopLeft;
return config;
}
public FocusTargetDebuffsListConfig(Vector2 position, Vector2 size, bool showBuffs, bool showDebuffs, bool showPermanentEffects,
GrowthDirections growthDirections, StatusEffectIconConfig iconConfig)
: base(position, size, showBuffs, showDebuffs, showPermanentEffects, growthDirections, iconConfig)
{
}
}
public abstract class UnitFrameStatusEffectsListConfig : StatusEffectsListConfig
{
[Checkbox("Anchor to Unit Frame")]
[Order(16)]
public bool AnchorToUnitFrame = false;
[Anchor("Unit Frame Anchor")]
[Order(17, collapseWith = nameof(AnchorToUnitFrame))]
public DrawAnchor UnitFrameAnchor = DrawAnchor.TopLeft;
[NestedConfig("Visibility", 200)]
public VisibilityConfig VisibilityConfig = new VisibilityConfig();
public UnitFrameStatusEffectsListConfig(Vector2 position, Vector2 size, bool showBuffs, bool showDebuffs, bool showPermanentEffects,
GrowthDirections growthDirections, StatusEffectIconConfig iconConfig)
: base(position, size, showBuffs, showDebuffs, showPermanentEffects, growthDirections, iconConfig)
{
}
}
public class StatusEffectsListConfig : MovablePluginConfigObject
{
public bool ShowBuffs;
public bool ShowDebuffs;
[DragInt2("Size", min = 1, max = 4000)]
[Order(15)]
public Vector2 Size;
[DragInt2("Icon Padding", min = 0, max = 500)]
[Order(19)]
public Vector2 IconPadding = new(2, 2);
[Checkbox("Preview", isMonitored = true)]
[Order(20)]
public bool Preview;
[Checkbox("Fill Rows First", spacing = true)]
[Order(25)]
public bool FillRowsFirst = true;
[Combo("Icons Growth Direction",
"Right and Down",
"Right and Up",
"Left and Down",
"Left and Up",
"Centered and Up",
"Centered and Down",
"Centered and Left",
"Centered and Right"
)]
[Order(30)]
public int Directions;
[DragInt("Limit (-1 for no limit)", min = -1, max = 1000)]
[Order(35)]
public int Limit = -1;
[Checkbox("Permanent Effects", spacing = true)]
[Order(40)]
public bool ShowPermanentEffects;
[Checkbox("Permanent Effects First")]
[Order(41)]
public bool ShowPermanentFirst;
[Checkbox("Only My Effects", spacing = true)]
[Order(42)]
public bool ShowOnlyMine = false;
[Checkbox("My Effects First")]
[Order(43)]
public bool ShowMineFirst = false;
[Checkbox("Pet As Own Effect")]
[Order(44)]
public bool IncludePetAsOwn = false;
[Checkbox("Sort by Duration", spacing = true, help = "If enabled, \"Permanent Effects First\" and \"My Effects First\" will be ignored!")]
[Order(45)]
public bool SortByDuration = false;
[RadioSelector("Ascending", "Descending")]
[Order(46, collapseWith = nameof(SortByDuration))]
public StatusEffectDurationSortType DurationSortType = StatusEffectDurationSortType.Ascending;
[Checkbox("Tooltips", spacing = true)]
[Order(47)]
public bool ShowTooltips = true;
[Checkbox("Disable Interaction", help = "Enabling this will disable right clicking buffs off, or the shortcut to blacklist/whitelist a status effect.")]
[Order(48)]
public bool DisableInteraction = false;
[Checkbox("Hide when Dead")]
[Order(49)]
public bool HideWhenDead = true;
[NestedConfig("Icons", 65)]
public StatusEffectIconConfig IconConfig;
[NestedConfig("Filter Status Effects", 70, separator = true, spacing = false, collapsingHeader = false)]
public StatusEffectsBlacklistConfig BlacklistConfig = new StatusEffectsBlacklistConfig();
public StatusEffectsListConfig(Vector2 position, Vector2 size, bool showBuffs, bool showDebuffs, bool showPermanentEffects,
GrowthDirections growthDirections, StatusEffectIconConfig iconConfig)
{
Position = position;
Size = size;
ShowBuffs = showBuffs;
ShowDebuffs = showDebuffs;
ShowPermanentEffects = showPermanentEffects;
SetGrowthDirections(growthDirections);
IconConfig = iconConfig;
Strata = StrataLevel.HIGH;
}
private void SetGrowthDirections(GrowthDirections growthDirections)
{
Directions = LayoutHelper.IndexFromGrowthDirections(growthDirections);
}
}
[Exportable(false)]
[Disableable(false)]
public class StatusEffectIconConfig : PluginConfigObject
{
[DragInt2("Icon Size", min = 1, max = 1000)]
[Order(5)]
public Vector2 Size = new(40, 40);
[Checkbox("Crop Icon", spacing = true)]
[Order(20)]
public bool CropIcon = true;
[NestedConfig("Border", 25, collapseWith = nameof(CropIcon), collapsingHeader = false)]
public StatusEffectIconBorderConfig BorderConfig = new();
[NestedConfig("Shadow", 26, collapseWith = nameof(CropIcon), collapsingHeader = false)]
public ShadowConfig ShadowConfig = new ShadowConfig() { Enabled = false };
[NestedConfig("Dispellable Effects Border", 30, collapseWith = nameof(CropIcon), collapsingHeader = false)]
public StatusEffectIconBorderConfig DispellableBorderConfig = new(new PluginConfigColor(new Vector4(141f / 255f, 206f / 255f, 229f / 255f, 100f / 100f)), 2);
[NestedConfig("My Effects Border", 35, collapseWith = nameof(CropIcon), collapsingHeader = false)]
public StatusEffectIconBorderConfig OwnedBorderConfig = new(new PluginConfigColor(new Vector4(35f / 255f, 179f / 255f, 69f / 255f, 100f / 100f)), 1);
[NestedConfig("Duration", 50)]
public LabelConfig DurationLabelConfig;
[NestedConfig("Stacks", 60)]
public LabelConfig StacksLabelConfig;
public StatusEffectIconConfig(LabelConfig? durationLabelConfig = null, LabelConfig? stacksLabelConfig = null)
{
DurationLabelConfig = durationLabelConfig ?? StatusEffectsListsDefaults.DefaultDurationLabelConfig();
StacksLabelConfig = stacksLabelConfig ?? StatusEffectsListsDefaults.DefaultStacksLabelConfig();
}
}
[Exportable(false)]
public class StatusEffectIconBorderConfig : PluginConfigObject
{
[ColorEdit4("Color")]
[Order(5)]
public PluginConfigColor Color = new(Vector4.UnitW);
[DragInt("Thickness", min = 1, max = 100)]
[Order(10)]
public int Thickness = 1;
public StatusEffectIconBorderConfig()
{
}
public StatusEffectIconBorderConfig(PluginConfigColor color, int thickness)
{
Color = color;
Thickness = thickness;
}
}
internal class StatusEffectsListsDefaults
{
internal static LabelConfig DefaultDurationLabelConfig()
{
return new LabelConfig(Vector2.Zero, "", DrawAnchor.Center, DrawAnchor.Center);
}
internal static LabelConfig DefaultStacksLabelConfig()
{
var config = new LabelConfig(new Vector2(16, -11), "", DrawAnchor.Center, DrawAnchor.Center);
config.Color = new(Vector4.UnitW);
config.OutlineColor = new(Vector4.One);
config.ShadowConfig.Color = new(Vector4.One);
return config;
}
}
public enum FilterType
{
Blacklist,
Whitelist
}
[Exportable(false)]
public class StatusEffectsBlacklistConfig : PluginConfigObject
{
[RadioSelector(typeof(FilterType))]
[Order(5)]
public FilterType FilterType;
public SortedList<string, uint> List = new SortedList<string, uint>();
[JsonIgnore] private string? _errorMessage = null;
[JsonIgnore] private string? _importString = null;
[JsonIgnore] private bool _clearingList = false;
private string KeyName(Status status)
{
return $"{status.Name}[{status.RowId.ToString()}]";
}
public bool StatusAllowed(Status status)
{
bool inList = List.Any(pair => pair.Value == status.RowId);
if ((inList && FilterType == FilterType.Blacklist) || (!inList && FilterType == FilterType.Whitelist))
{
return false;
}
return true;
}
public bool AddNewEntry(Status? status)
{
if (status != null && status.HasValue && !List.ContainsKey(KeyName(status.Value)))
{
List.Add(KeyName(status.Value), status.Value.RowId);
_input = "";
return true;
}
return false;
}
private bool AddNewEntry(string input, ExcelSheet<Status>? sheet)
{
if (input.Length > 0 && sheet != null)
{
List<Status> statusToAdd = new List<Status>();
// try id
if (uint.TryParse(input, out uint uintValue))
{
if (uintValue > 0)
{
Status? status = sheet.GetRow(uintValue);
if (status != null && status.HasValue)
{
statusToAdd.Add(status.Value);
}
}
}
// try name
if (statusToAdd.Count == 0)
{
var enumerator = sheet.GetEnumerator();
while (enumerator.MoveNext())
{
Status item = enumerator.Current;
if (item.Name.ToString().ToLower() == input.ToLower())
{
statusToAdd.Add(item);
}
}
}
bool added = false;
foreach (Status status in statusToAdd)
{
added |= AddNewEntry(status);
}
return added;
}
return false;
}
private string ExportList()
{
string exportString = "";
for (int i = 0; i < List.Keys.Count; i++)
{
exportString += List.Keys[i] + "|";
exportString += List.Values[i] + "|";
}
return exportString;
}
private string? ImportList(string importString)
{
SortedList<string, uint> tmpList = new SortedList<string, uint>();
try
{
string[] strings = importString.Trim().Split("|", StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i < strings.Length; i += 2)
{
if (i + 1 >= strings.Length)
{
break;
}
string key = strings[i];
uint value = uint.Parse(strings[i + 1]);
tmpList.Add(key, value);
}
}
catch
{
return "Error importing list!";
}
List = tmpList;
return null;
}
[JsonIgnore]
private string _input = "";
[ManualDraw]
public bool Draw(ref bool changed)
{
if (!Enabled)
{
return false;
}
var flags =
ImGuiTableFlags.RowBg |
ImGuiTableFlags.Borders |
ImGuiTableFlags.BordersOuter |
ImGuiTableFlags.BordersInner |
ImGuiTableFlags.ScrollY |
ImGuiTableFlags.SizingFixedSame;
var sheet = Plugin.DataManager.GetExcelSheet<Status>();
var iconSize = new Vector2(30, 30);
var indexToRemove = -1;
if (ImGui.BeginChild("Filter Effects", new Vector2(0, 360), false, ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse))
{
ImGui.Text(" ");
ImGui.SameLine();
ImGui.Text("Type an ID or Name");
ImGui.Text(" ");
ImGui.SameLine();
ImGui.PushItemWidth(300);
if (ImGui.InputText("", ref _input, 64, ImGuiInputTextFlags.EnterReturnsTrue))
{
changed |= AddNewEntry(_input, sheet);
ImGui.SetKeyboardFocusHere(-1);
}
// add
ImGui.SameLine();
ImGui.PushFont(UiBuilder.IconFont);
if (ImGui.Button(FontAwesomeIcon.Plus.ToIconString(), new Vector2(0, 0)))
{
changed |= AddNewEntry(_input, sheet);
ImGui.SetKeyboardFocusHere(-2);
}
// export
ImGui.SameLine();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 154);
if (ImGui.Button(FontAwesomeIcon.Upload.ToIconString(), new Vector2(0, 0)))
{
ImGui.SetClipboardText(ExportList());
ImGui.OpenPopup("export_succes_popup");
}
ImGui.PopFont();
ImGuiHelper.SetTooltip("Export List to Clipboard");
// export success popup
if (ImGui.BeginPopup("export_succes_popup"))
{
ImGui.Text("List exported to clipboard!");
ImGui.EndPopup();
}
// import
ImGui.SameLine();
ImGui.PushFont(UiBuilder.IconFont);
if (ImGui.Button(FontAwesomeIcon.Download.ToIconString(), new Vector2(0, 0)))
{
_importString = ImGui.GetClipboardText();
}
ImGui.PopFont();
ImGuiHelper.SetTooltip("Import List from Clipboard");
// clear
ImGui.SameLine();
ImGui.PushFont(UiBuilder.IconFont);
if (ImGui.Button(FontAwesomeIcon.Trash.ToIconString(), new Vector2(0, 0)))
{
_clearingList = true;
}
ImGui.PopFont();
ImGuiHelper.SetTooltip("Clear List");
ImGui.Text(" ");
ImGui.SameLine();
if (ImGui.BeginTable("table", 4, flags, new Vector2(583, List.Count > 0 ? 200 : 40)))
{
ImGui.TableSetupColumn("Icon", ImGuiTableColumnFlags.WidthFixed, 0, 0);
ImGui.TableSetupColumn("ID", ImGuiTableColumnFlags.WidthFixed, 0, 1);
ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch, 0, 2);
ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 0, 3);
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableHeadersRow();
for (int i = 0; i < List.Count; i++)
{
var id = List.Values[i];
var name = List.Keys[i];
var row = sheet?.GetRow(id);
if (_input != "" && !name.ToUpper().Contains(_input.ToUpper()))
{
continue;
}
ImGui.PushID(i.ToString());
ImGui.TableNextRow(ImGuiTableRowFlags.None, iconSize.Y);
// icon
if (ImGui.TableSetColumnIndex(0))
{
if (row != null)
{
DrawHelper.DrawIcon<Status>(row, ImGui.GetCursorPos(), iconSize, false, true);
}
}
// id
if (ImGui.TableSetColumnIndex(1))
{
ImGui.Text(id.ToString());
}
// name
if (ImGui.TableSetColumnIndex(2))
{
ImGui.Text(row != null && row.HasValue ? row.Value.Name.ToString() : name);
}
// remove
if (ImGui.TableSetColumnIndex(3))
{
ImGui.PushFont(UiBuilder.IconFont);
ImGui.PushStyleColor(ImGuiCol.Button, Vector4.Zero);
ImGui.PushStyleColor(ImGuiCol.ButtonActive, Vector4.Zero);
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, Vector4.Zero);
if (ImGui.Button(FontAwesomeIcon.Trash.ToIconString(), iconSize))
{
changed = true;
indexToRemove = i;
}
ImGui.PopFont();
ImGui.PopStyleColor(3);
}
ImGui.PopID();
}
ImGui.EndTable();
}
ImGui.Text(" ");
ImGui.SameLine();
ImGui.Text("Tip: You can [Ctrl + Alt + Shift] + Left Click on a status effect to automatically add it to the list.");
}
if (indexToRemove >= 0)
{
List.RemoveAt(indexToRemove);
}
ImGui.EndChild();
// error message
if (_errorMessage != null)
{
if (ImGuiHelper.DrawErrorModal(_errorMessage))
{
_errorMessage = null;
}
}
// import confirmation
if (_importString != null)
{
string[] message = new string[] {
"All the elements in the list will be replaced.",
"Are you sure you want to import?"
};
var (didConfirm, didClose) = ImGuiHelper.DrawConfirmationModal("Import?", message);
if (didConfirm)
{
_errorMessage = ImportList(_importString);
changed = true;
}
if (didConfirm || didClose)
{
_importString = null;
}
}
// clear confirmation
if (_clearingList)
{
string message = "Are you sure you want to clear the list?";
var (didConfirm, didClose) = ImGuiHelper.DrawConfirmationModal("Clear List?", message);
if (didConfirm)
{
List.Clear();
changed = true;
}
if (didConfirm || didClose)
{
_clearingList = false;
}
}
return false;
}
}
public class StatusEffectsBlacklistConfigConverter : PluginConfigObjectConverter
{
public StatusEffectsBlacklistConfigConverter()
{
NewTypeFieldConverter<bool, FilterType> converter;
converter = new NewTypeFieldConverter<bool, FilterType>("FilterType", FilterType.Blacklist, (oldValue) =>
{
return oldValue ? FilterType.Whitelist : FilterType.Blacklist;
});
FieldConvertersMap.Add("UseAsWhitelist", converter);
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(StatusEffectsBlacklistConfig);
}
}
[Section("Buffs and Debuffs")]
[SubSection("Custom Effects", 0)]
public class CustomEffectsListConfig : StatusEffectsListConfig
{
public new static CustomEffectsListConfig DefaultConfig()
{
var iconConfig = new StatusEffectIconConfig();
iconConfig.DispellableBorderConfig.Enabled = false;
iconConfig.Size = new Vector2(30, 30);
var pos = new Vector2(0, HUDConstants.BaseHUDOffsetY);
var size = new Vector2(250, iconConfig.Size.Y * 3 + 10);
var config = new CustomEffectsListConfig(pos, size, true, true, false, GrowthDirections.Centered | GrowthDirections.Up, iconConfig);
config.Enabled = false;
config.Directions = 5;
// pre-populated white list
config.BlacklistConfig.FilterType = FilterType.Whitelist;
ExcelSheet<Status>? sheet = Plugin.DataManager.GetExcelSheet<Status>();
if (sheet != null)
{
// Left Eye
config.BlacklistConfig.AddNewEntry(sheet.GetRow(1184));
config.BlacklistConfig.AddNewEntry(sheet.GetRow(1454));
// Battle Litany
config.BlacklistConfig.AddNewEntry(sheet.GetRow(786));
config.BlacklistConfig.AddNewEntry(sheet.GetRow(1414));
// Brotherhood
config.BlacklistConfig.AddNewEntry(sheet.GetRow(1185));
config.BlacklistConfig.AddNewEntry(sheet.GetRow(2174));
// Battle Voice
config.BlacklistConfig.AddNewEntry(sheet.GetRow(141));
// Devilment
config.BlacklistConfig.AddNewEntry(sheet.GetRow(1825));
// Technical Finish
config.BlacklistConfig.AddNewEntry(sheet.GetRow(1822));
config.BlacklistConfig.AddNewEntry(sheet.GetRow(2050));
// Standard Finish
config.BlacklistConfig.AddNewEntry(sheet.GetRow(1821));
config.BlacklistConfig.AddNewEntry(sheet.GetRow(2024));
config.BlacklistConfig.AddNewEntry(sheet.GetRow(2105));
config.BlacklistConfig.AddNewEntry(sheet.GetRow(2113));
// Embolden
config.BlacklistConfig.AddNewEntry(sheet.GetRow(1239));
config.BlacklistConfig.AddNewEntry(sheet.GetRow(1297));
config.BlacklistConfig.AddNewEntry(sheet.GetRow(2282));
// Devotion
config.BlacklistConfig.AddNewEntry(sheet.GetRow(1213));
// Divination
config.BlacklistConfig.AddNewEntry(sheet.GetRow(1878));
config.BlacklistConfig.AddNewEntry(sheet.GetRow(2034));
// Chain Stratagem
config.BlacklistConfig.AddNewEntry(sheet.GetRow(1221));
config.BlacklistConfig.AddNewEntry(sheet.GetRow(1406));
// Radiant Finale
config.BlacklistConfig.AddNewEntry(sheet.GetRow(2722));
config.BlacklistConfig.AddNewEntry(sheet.GetRow(2964));
// Arcane Circle
config.BlacklistConfig.AddNewEntry(sheet.GetRow(2599));
// Searing Light
config.BlacklistConfig.AddNewEntry(sheet.GetRow(2703));
// Trick Attack
config.BlacklistConfig.AddNewEntry(sheet.GetRow(638));
// ------ AST Card Buffs -------
// The Balance
config.BlacklistConfig.AddNewEntry(sheet.GetRow(829));
config.BlacklistConfig.AddNewEntry(sheet.GetRow(1338));
config.BlacklistConfig.AddNewEntry(sheet.GetRow(1882));
// The Bole
config.BlacklistConfig.AddNewEntry(sheet.GetRow(830));
config.BlacklistConfig.AddNewEntry(sheet.GetRow(1339));
config.BlacklistConfig.AddNewEntry(sheet.GetRow(1883));
// The Arrow
config.BlacklistConfig.AddNewEntry(sheet.GetRow(831));
config.BlacklistConfig.AddNewEntry(sheet.GetRow(1884));
// The Spear
config.BlacklistConfig.AddNewEntry(sheet.GetRow(832));
config.BlacklistConfig.AddNewEntry(sheet.GetRow(1885));
// The Ewer
config.BlacklistConfig.AddNewEntry(sheet.GetRow(833));
config.BlacklistConfig.AddNewEntry(sheet.GetRow(1340));
config.BlacklistConfig.AddNewEntry(sheet.GetRow(1886));
// The Spire
config.BlacklistConfig.AddNewEntry(sheet.GetRow(834));
config.BlacklistConfig.AddNewEntry(sheet.GetRow(1341));
config.BlacklistConfig.AddNewEntry(sheet.GetRow(1887));
}
return config;
}
public CustomEffectsListConfig(Vector2 position, Vector2 size, bool showBuffs, bool showDebuffs, bool showPermanentEffects,
GrowthDirections growthDirections, StatusEffectIconConfig iconConfig)
: base(position, size, showBuffs, showDebuffs, showPermanentEffects, growthDirections, iconConfig)
{
}
}
public enum StatusEffectDurationSortType
{
Ascending,
Descending
}
}
@@ -0,0 +1,589 @@
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Utility;
using HSUI.Config;
using HSUI.Enums;
using HSUI.Helpers;
using HSUI.Interface.GeneralElements;
using Dalamud.Bindings.ImGui;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using FFXIVClientStructs.FFXIV.Client.Game;
using LuminaStatus = Lumina.Excel.Sheets.Status;
using StatusStruct = FFXIVClientStructs.FFXIV.Client.Game.Status;
namespace HSUI.Interface.StatusEffects
{
public class StatusEffectsListHud : ParentAnchoredDraggableHudElement, IHudElementWithActor, IHudElementWithAnchorableParent, IHudElementWithPreview, IHudElementWithMouseOver, IHudElementWithVisibilityConfig
{
protected StatusEffectsListConfig Config => (StatusEffectsListConfig)_config;
public VisibilityConfig? VisibilityConfig => Config is UnitFrameStatusEffectsListConfig config ? config.VisibilityConfig : null;
private LayoutInfo _layoutInfo;
internal static int StatusEffectListsSize = 60;
private StatusStruct[]? _fakeEffects = null;
private LabelHud _durationLabel;
private LabelHud _stacksLabel;
public IGameObject? Actor { get; set; } = null;
private bool _wasHovering = false;
private bool NeedsSpecialInput => !ClipRectsHelper.Instance.Enabled || ClipRectsHelper.Instance.Mode == WindowClippingMode.Performance;
protected override bool AnchorToParent => Config is UnitFrameStatusEffectsListConfig config ? config.AnchorToUnitFrame : false;
protected override DrawAnchor ParentAnchor => Config is UnitFrameStatusEffectsListConfig config ? config.UnitFrameAnchor : DrawAnchor.Center;
public StatusEffectsListHud(StatusEffectsListConfig config, string? displayName = null) : base(config, displayName)
{
_config.ValueChangeEvent += OnConfigPropertyChanged;
_durationLabel = new LabelHud(config.IconConfig.DurationLabelConfig);
_stacksLabel = new LabelHud(config.IconConfig.StacksLabelConfig);
UpdatePreview();
}
~StatusEffectsListHud()
{
_config.ValueChangeEvent -= OnConfigPropertyChanged;
}
public void StopPreview()
{
Config.Preview = false;
UpdatePreview();
}
protected override (List<Vector2>, List<Vector2>) ChildrenPositionsAndSizes()
{
Vector2 pos = LayoutHelper.CalculateStartPosition(Config.Position, Config.Size, LayoutHelper.GrowthDirectionsFromIndex(Config.Directions));
return (new List<Vector2>() { pos + Config.Size / 2f }, new List<Vector2>() { Config.Size });
}
public void StopMouseover()
{
if (_wasHovering && NeedsSpecialInput)
{
InputsHelper.Instance.StopHandlingInputs();
_wasHovering = false;
}
}
private uint CalculateLayout(List<StatusEffectData> list)
{
var effectCount = (uint)list.Count;
var count = Config.Limit >= 0 ? Math.Min((uint)Config.Limit, effectCount) : effectCount;
if (count <= 0)
{
return 0;
}
_layoutInfo = LayoutHelper.CalculateLayout(
Config.Size,
Config.IconConfig.Size,
count,
Config.IconPadding,
LayoutHelper.GetFillsRowsFirst(Config.FillRowsFirst, LayoutHelper.GrowthDirectionsFromIndex(Config.Directions))
);
return count;
}
protected string GetStatusActorName(StatusStruct status)
{
var character = Plugin.ObjectTable.SearchById(status.SourceObject.Id);
return character == null ? "" : character.Name.ToString();
}
protected virtual List<StatusEffectData> StatusEffectsData()
{
var list = StatusEffectDataList(Actor);
// sort by duration
if (Config.SortByDuration)
{
list.Sort((a, b) =>
{
float aTime = a.Data.IsPermanent || a.Data.IsFcBuff ? float.MaxValue : a.Status.RemainingTime;
float bTime = b.Data.IsPermanent || b.Data.IsFcBuff ? float.MaxValue : b.Status.RemainingTime;
if (Config.DurationSortType == StatusEffectDurationSortType.Ascending)
{
return aTime.CompareTo(bTime);
}
else
{
return bTime.CompareTo(aTime);
}
});
}
// show mine or permanent first
else if (Config.ShowMineFirst || Config.ShowPermanentFirst)
{
return OrderByMineOrPermanentFirst(list);
}
return list;
}
protected unsafe List<StatusEffectData> StatusEffectDataList(IGameObject? actor)
{
List<StatusEffectData> list = new List<StatusEffectData>();
IPlayerCharacter? player = Plugin.ObjectTable.LocalPlayer;
IBattleChara? character = null;
int count = StatusEffectListsSize;
if (_fakeEffects == null)
{
if (actor == null || actor is not IBattleChara battleChara)
{
return list;
}
if (Config.HideWhenDead && (battleChara.IsDead || battleChara.CurrentHp <= 0))
{
return list;
}
character = (IBattleChara)actor;
try
{
count = Math.Min(count, character.StatusList.Length);
}
catch { }
}
else
{
count = Config.Limit == -1 ? _fakeEffects.Length : Math.Min(Config.Limit, _fakeEffects.Length);
}
for (int i = 0; i < count; i++)
{
// status
StatusStruct* status = null;
if (_fakeEffects != null)
{
var fakeStruct = _fakeEffects![i];
status = &fakeStruct;
}
else
{
try
{
status = character?.StatusList[i] == null ? null : (StatusStruct*)character.StatusList[i]!.Address;
}
catch { }
}
if (status == null || status->StatusId == 0)
{
continue;
}
// data
LuminaStatus? data = null;
if (_fakeEffects != null)
{
data = Plugin.DataManager.GetExcelSheet<LuminaStatus>()?.GetRow(status->StatusId);
}
else
{
try
{
data = character?.StatusList[i]?.GameData.Value;
} catch { }
}
if (data == null || !data.HasValue)
{
continue;
}
// filter "invisible" status effects
if (data.Value.Icon == 0 || data.Value.Name.ToString().Length == 0)
{
continue;
}
// dont filter anything on preview mode
if (_fakeEffects != null)
{
list.Add(new StatusEffectData(*status, data.Value));
continue;
}
// buffs
if (!Config.ShowBuffs && data.Value.StatusCategory == 1)
{
continue;
}
// debuffs
if (!Config.ShowDebuffs && data.Value.StatusCategory != 1)
{
continue;
}
// permanent
if (!Config.ShowPermanentEffects && data.Value.IsPermanent)
{
continue;
}
// only mine
var mine = player?.GameObjectId == status->SourceObject.Id;
if (Config.IncludePetAsOwn)
{
mine = player?.GameObjectId == status->SourceObject.Id || IsStatusFromPlayerPet(*status);
}
if (Config.ShowOnlyMine && !mine)
{
continue;
}
// blacklist
if (Config.BlacklistConfig.Enabled && !Config.BlacklistConfig.StatusAllowed(data.Value))
{
continue;
}
list.Add(new StatusEffectData(*status, data.Value));
}
return list;
}
protected bool IsStatusFromPlayerPet(StatusStruct status)
{
var buddy = Plugin.BuddyList.PetBuddy;
if (buddy == null)
{
return false;
}
return buddy.EntityId == status.SourceObject.Id;
}
protected List<StatusEffectData> OrderByMineOrPermanentFirst(List<StatusEffectData> list)
{
var player = Plugin.ObjectTable.LocalPlayer;
if (player == null)
{
return list;
}
if (Config.ShowMineFirst && Config.ShowPermanentFirst)
{
return list.OrderByDescending(x => x.Status.SourceObject.Id == player.GameObjectId && x.Data.IsPermanent || x.Data.IsFcBuff)
.ThenByDescending(x => x.Status.SourceObject.Id == player.GameObjectId)
.ThenByDescending(x => x.Data.IsPermanent)
.ThenByDescending(x => x.Data.IsFcBuff)
.ToList();
}
else if (Config.ShowMineFirst && !Config.ShowPermanentFirst)
{
return list.OrderByDescending(x => x.Status.SourceObject.Id == player.GameObjectId)
.ToList();
}
else if (!Config.ShowMineFirst && Config.ShowPermanentFirst)
{
return list.OrderByDescending(x => x.Data.IsPermanent)
.ThenByDescending(x => x.Data.IsFcBuff)
.ToList();
}
return list;
}
public override void DrawChildren(Vector2 origin)
{
if (!Config.Enabled)
{
return;
}
if (_fakeEffects == null && (Actor == null || Actor.ObjectKind != ObjectKind.Player && Actor.ObjectKind != ObjectKind.BattleNpc))
{
return;
}
// calculate layout
List<StatusEffectData> list = StatusEffectsData();
// area
GrowthDirections growthDirections = LayoutHelper.GrowthDirectionsFromIndex(Config.Directions);
Vector2 position = origin + GetAnchoredPosition(Config.Position, Config.Size, DrawAnchor.TopLeft);
Vector2 areaPos = LayoutHelper.CalculateStartPosition(position, Config.Size, growthDirections);
Vector2 margin = new Vector2(14, 10);
ImDrawListPtr drawList = ImGui.GetWindowDrawList();
// no need to do anything else if there are no effects
if (list.Count == 0)
{
if (_wasHovering && NeedsSpecialInput)
{
_wasHovering = false;
InputsHelper.Instance.StopHandlingInputs();
}
return;
}
// calculate icon positions
uint count = CalculateLayout(list);
var (iconPositions, minPos, maxPos) = LayoutHelper.CalculateIconPositions(
growthDirections,
count,
position,
Config.Size,
Config.IconConfig.Size,
Config.IconPadding,
LayoutHelper.GetFillsRowsFirst(Config.FillRowsFirst, growthDirections),
_layoutInfo
);
// window
// 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
Vector2 windowPos = minPos - margin;
Vector2 windowSize = maxPos - minPos;
AddDrawAction(Config.StrataLevel, () =>
{
DrawHelper.DrawInWindow(ID, windowPos, windowSize + margin * 2, !Config.DisableInteraction, (drawList) =>
{
// area
if (Config.Preview)
{
drawList.AddRectFilled(areaPos, areaPos + Config.Size, 0x88000000);
}
for (var i = 0; i < count; i++)
{
Vector2 iconPos = iconPositions[i];
var statusEffectData = list[i];
// shadow
if (Config.IconConfig.ShadowConfig! != null && Config.IconConfig.ShadowConfig.Enabled)
{
// Right Side
drawList.AddRectFilled(iconPos + new Vector2(Config.IconConfig.Size.X, Config.IconConfig.ShadowConfig.Offset), iconPos + Config.IconConfig.Size + new Vector2(Config.IconConfig.ShadowConfig.Offset, Config.IconConfig.ShadowConfig.Offset) + new Vector2(Config.IconConfig.ShadowConfig.Thickness - 1, Config.IconConfig.ShadowConfig.Thickness - 1), Config.IconConfig.ShadowConfig.Color.Base);
// Bottom Size
drawList.AddRectFilled(iconPos + new Vector2(Config.IconConfig.ShadowConfig.Offset, Config.IconConfig.Size.Y), iconPos + Config.IconConfig.Size + new Vector2(Config.IconConfig.ShadowConfig.Offset, Config.IconConfig.ShadowConfig.Offset) + new Vector2(Config.IconConfig.ShadowConfig.Thickness - 1, Config.IconConfig.ShadowConfig.Thickness - 1), Config.IconConfig.ShadowConfig.Color.Base);
}
// icon
var cropIcon = Config.IconConfig.CropIcon;
int stackCount = cropIcon ? 1 : statusEffectData.Data.MaxStacks > 0 ? statusEffectData.Status.Param : 0;
DrawHelper.DrawIcon<LuminaStatus>(drawList, statusEffectData.Data, iconPos, Config.IconConfig.Size, false, cropIcon, stackCount);
// border
var borderConfig = GetBorderConfig(statusEffectData);
if (borderConfig != null && cropIcon)
{
drawList.AddRect(iconPos, iconPos + Config.IconConfig.Size, borderConfig.Color.Base, 0, ImDrawFlags.None, borderConfig.Thickness);
}
// Draw dispell indicator above dispellable status effect on uncropped icons
if (borderConfig != null && !cropIcon && statusEffectData.Data.CanDispel)
{
var dispellIndicatorColor = new Vector4(141f / 255f, 206f / 255f, 229f / 255f, 100f / 100f);
// 24x32
drawList.AddRectFilled(
iconPos + new Vector2(Config.IconConfig.Size.X * .07f, Config.IconConfig.Size.Y * .07f),
iconPos + new Vector2(Config.IconConfig.Size.X * .93f, Config.IconConfig.Size.Y * .14f),
ImGui.ColorConvertFloat4ToU32(dispellIndicatorColor),
8f
);
}
}
});
});
StatusEffectData? hoveringData = null;
IGameObject? character = Actor;
// labels need to be drawn separated since they have their own window for clipping
for (var i = 0; i < count; i++)
{
Vector2 iconPos = iconPositions[i];
StatusEffectData statusEffectData = list[i];
// duration
if (Config.IconConfig.DurationLabelConfig.Enabled &&
!statusEffectData.Data.IsPermanent &&
!statusEffectData.Data.IsFcBuff)
{
AddDrawAction(Config.IconConfig.DurationLabelConfig.StrataLevel, () =>
{
double duration = Math.Round(Math.Abs(statusEffectData.Status.RemainingTime));
Config.IconConfig.DurationLabelConfig.SetText(Utils.DurationToString(duration));
_durationLabel.Draw(iconPos, Config.IconConfig.Size, character);
});
}
// stacks
if (Config.IconConfig.StacksLabelConfig.Enabled &&
statusEffectData.Data.MaxStacks > 0 &&
statusEffectData.Status.Param > 0 &&
!statusEffectData.Data.IsFcBuff)
{
AddDrawAction(Config.IconConfig.StacksLabelConfig.StrataLevel, () =>
{
Config.IconConfig.StacksLabelConfig.SetText($"{statusEffectData.Status.Param}");
_stacksLabel.Draw(iconPos, Config.IconConfig.Size, character);
});
}
// tooltips / interaction
if (ImGui.IsMouseHoveringRect(iconPos, iconPos + Config.IconConfig.Size))
{
hoveringData = statusEffectData;
}
}
if (hoveringData.HasValue)
{
StatusEffectData data = hoveringData.Value;
if (NeedsSpecialInput)
{
_wasHovering = true;
InputsHelper.Instance.StartHandlingInputs();
}
// tooltip
if (Config.ShowTooltips)
{
TooltipsHelper.Instance.ShowTooltipOnCursor(
EncryptedStringsHelper.GetString(data.Data.Description.ToDalamudString().ToString()),
EncryptedStringsHelper.GetString(data.Data.Name.ToString()),
data.Status.StatusId,
GetStatusActorName(data.Status)
);
}
bool leftClick = InputsHelper.Instance.HandlingMouseInputs ? InputsHelper.Instance.LeftButtonClicked : ImGui.GetIO().MouseClicked[0];
bool rightClick = InputsHelper.Instance.HandlingMouseInputs ? InputsHelper.Instance.RightButtonClicked : ImGui.GetIO().MouseClicked[1];
// remove buff on right click
bool isFromPlayer = data.Status.SourceObject.Id == Plugin.ObjectTable.LocalPlayer?.GameObjectId;
bool isTheEcho = data.Status.SourceObject.Id is 42 or 239;
if (data.Data.StatusCategory == 1 && (isFromPlayer || isTheEcho) && rightClick)
{
StatusManager.ExecuteStatusOff(data.Status.StatusId, data.Status.SourceObject.ObjectId);
if (NeedsSpecialInput)
{
_wasHovering = false;
InputsHelper.Instance.StopHandlingInputs();
}
}
// automatic add to black list with ctrl+alt+shift click
if (Config.BlacklistConfig.Enabled &&
ImGui.GetIO().KeyCtrl && ImGui.GetIO().KeyAlt && ImGui.GetIO().KeyShift && leftClick)
{
Config.BlacklistConfig.AddNewEntry(data.Data);
ConfigurationManager.Instance.ForceNeedsSave();
if (NeedsSpecialInput)
{
_wasHovering = false;
InputsHelper.Instance.StopHandlingInputs();
}
}
}
else if (_wasHovering && NeedsSpecialInput)
{
_wasHovering = false;
InputsHelper.Instance.StopHandlingInputs();
}
}
public StatusEffectIconBorderConfig? GetBorderConfig(StatusEffectData statusEffectData)
{
StatusEffectIconBorderConfig? borderConfig = null;
bool isFromPlayerPet = false;
if (Config.IncludePetAsOwn)
{
isFromPlayerPet = IsStatusFromPlayerPet(statusEffectData.Status);
}
if (Config.IconConfig.OwnedBorderConfig.Enabled && (statusEffectData.Status.SourceObject.Id == Plugin.ObjectTable.LocalPlayer?.GameObjectId || isFromPlayerPet))
{
borderConfig = Config.IconConfig.OwnedBorderConfig;
}
else if (Config.IconConfig.DispellableBorderConfig.Enabled && statusEffectData.Data.CanDispel)
{
borderConfig = Config.IconConfig.DispellableBorderConfig;
}
else if (Config.IconConfig.BorderConfig.Enabled)
{
borderConfig = Config.IconConfig.BorderConfig;
}
return borderConfig;
}
private void OnConfigPropertyChanged(object? sender, OnChangeBaseArgs args)
{
if (args.PropertyName == "Preview")
{
UpdatePreview();
}
}
private unsafe void UpdatePreview()
{
if (!Config.Preview)
{
_fakeEffects = null;
return;
}
var RNG = new Random((int)ImGui.GetTime());
_fakeEffects = new StatusStruct[StatusEffectListsSize];
for (int i = 0; i < StatusEffectListsSize; i++)
{
var fakeStruct = new StatusStruct();
// forcing "triplecast" buff first to always be able to test stacks
fakeStruct.StatusId = i == 0 ? (ushort)1211 : (ushort)RNG.Next(1, 200);
fakeStruct.RemainingTime = RNG.Next(1, 30);
fakeStruct.Param = (byte)RNG.Next(1, 3);
fakeStruct.SourceObject.Id = 0;
_fakeEffects[i] = fakeStruct;
}
}
}
public struct StatusEffectData
{
public StatusStruct Status;
public LuminaStatus Data;
public StatusEffectData(StatusStruct status, LuminaStatus data)
{
Status = status;
Data = data;
}
}
}