Files
HSUI/Interface/StatusEffects/StatusEffectsListHud.cs
T

592 lines
23 KiB
C#

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)
{
uint? iconId = data.Data.Icon > 0 ? data.Data.Icon : null;
TooltipsHelper.Instance.ShowTooltipOnCursor(
EncryptedStringsHelper.GetString(data.Data.Description.ToDalamudString().ToString()),
EncryptedStringsHelper.GetString(data.Data.Name.ToString()),
data.Status.StatusId,
GetStatusActorName(data.Status),
iconId
);
}
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;
}
}
}