commit f37369cdda54cb77e43d36b9d9e6a597a55968c7 Author: Knack117 Date: Fri Jan 30 23:52:46 2026 -0500 Initial release: HSUI v1.0.0.0 - HUD replacement with configurable hotbars Co-authored-by: Cursor diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..25ef8a4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Build results +bin/ +obj/ +[Bb]uild/ +[Dd]ebug/ +[Rr]elease/ + +# Visual Studio +.vs/ +*.user +*.suo +*.cache + +# Rider +.idea/ + +# User-specific +*.rsuser +*.userprefs + +# NuGet +packages/ +*.nupkg + +# Misc +*.log +*.tmp +*.temp diff --git a/Config/Attributes/ConfigTypeAttributes.cs b/Config/Attributes/ConfigTypeAttributes.cs new file mode 100644 index 0000000..4561c72 --- /dev/null +++ b/Config/Attributes/ConfigTypeAttributes.cs @@ -0,0 +1,682 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility; +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 System.Reflection; + +namespace HSUI.Config.Attributes +{ + #region class attributes + [AttributeUsage(AttributeTargets.Class)] + public class ExportableAttribute : Attribute + { + public bool exportable; + + public ExportableAttribute(bool exportable) + { + this.exportable = exportable; + } + } + + public class ShareableAttribute : Attribute + { + public bool shareable; + + public ShareableAttribute(bool shareable) + { + this.shareable = shareable; + } + } + + public class ResettableAttribute : Attribute + { + public bool resettable; + + public ResettableAttribute(bool resettable) + { + this.resettable = resettable; + } + } + + [AttributeUsage(AttributeTargets.Class)] + public class DisableableAttribute : Attribute + { + public bool disableable; + + public DisableableAttribute(bool disableable) + { + this.disableable = disableable; + } + } + + [AttributeUsage(AttributeTargets.Class)] + public class DisableParentSettingsAttribute : Attribute + { + public readonly string[] DisabledFields; + + public DisableParentSettingsAttribute(params string[] fields) + { + this.DisabledFields = fields; + } + } + #endregion + + #region method attributes + [AttributeUsage(AttributeTargets.Method)] + public class ManualDrawAttribute : Attribute + { + } + #endregion + + #region field attributes + [AttributeUsage(AttributeTargets.Field)] + public abstract class ConfigAttribute : Attribute + { + public string friendlyName; + public bool isMonitored = false; + public bool separator = false; + public bool spacing = false; + public string? help = null; + + public ConfigAttribute(string friendlyName) + { + this.friendlyName = friendlyName; + } + + public bool Draw(FieldInfo field, PluginConfigObject config, string? ID, bool collapsingHeader = false) + { + bool result = DrawField(field, config, ID, collapsingHeader); + + if (help != null && ImGui.IsItemHovered()) + { + ImGui.SetTooltip(help); + } + + return result; + } + + public abstract bool DrawField(FieldInfo field, PluginConfigObject config, string? ID, bool collapsingHeader = false); + + protected string IDText(string? ID) => ID != null ? " ##" + ID : ""; + + protected void TriggerChangeEvent(PluginConfigObject config, string fieldName, object value, ChangeType type = ChangeType.None) + { + if (!isMonitored || config is not IOnChangeEventArgs eventObject) + { + return; + } + + eventObject.OnValueChanged(new OnChangeEventArgs(fieldName, (T)value, type)); + } + } + + [AttributeUsage(AttributeTargets.Field)] + public class CheckboxAttribute : ConfigAttribute + { + public CheckboxAttribute(string friendlyName) : base(friendlyName) { } + + public override bool DrawField(FieldInfo field, PluginConfigObject config, string? ID, bool collapsingHeader) + { + var disableable = config.Disableable; + + if (!disableable && friendlyName == "Enabled") + { + if (ID != null) + { + ImGui.Text(ID); + } + return false; + } + + bool? fieldVal = (bool?)field.GetValue(config); + bool boolVal = fieldVal.HasValue ? fieldVal.Value : false; + + if (ImGui.Checkbox(ID != null && friendlyName == "Enabled" && !collapsingHeader ? ID : friendlyName + IDText(ID), ref boolVal)) + { + field.SetValue(config, boolVal); + + TriggerChangeEvent(config, field.Name, boolVal); + + return true; + } + + return false; + } + } + + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] + public class RadioSelector : ConfigAttribute + { + private string[] _options; + + public string? Label { get; set; } + + public RadioSelector(params string[] options) : base(string.Join("_", options)) + { + _options = options; + } + + public RadioSelector(Type enumType) : this(enumType.IsEnum ? Enum.GetNames(enumType) : Array.Empty()) { } + + public override bool DrawField(FieldInfo field, PluginConfigObject config, string? ID, bool collapsingHeader) + { + bool changed = false; + object? fieldVal = field.GetValue(config); + + int intVal = 0; + if (fieldVal != null) + { + intVal = (int)fieldVal; + } + + string? label = Label ?? friendlyName; + if (!string.IsNullOrEmpty(label) && label != string.Join("_", _options)) + { + ImGui.TextUnformatted(label); + ImGui.SameLine(); + } + + for (int i = 0; i < _options.Length; i++) + { + changed |= ImGui.RadioButton(_options[i], ref intVal, i); + if (i < _options.Length - 1) + { + ImGui.SameLine(); + } + } + + if (changed) + { + field.SetValue(config, intVal); + TriggerChangeEvent(config, field.Name, intVal); + } + + return changed; + } + } + + [AttributeUsage(AttributeTargets.Field)] + public class DragFloatAttribute : ConfigAttribute + { + public float min; + public float max; + public float velocity; + + public DragFloatAttribute(string friendlyName) : base(friendlyName) + { + min = 1f; + max = 1000f; + velocity = 1f; + } + + public override bool DrawField(FieldInfo field, PluginConfigObject config, string? ID, bool collapsingHeader) + { + float? fieldVal = (float?)field.GetValue(config); + float floatVal = fieldVal.HasValue ? fieldVal.Value : 0; + + if (ImGui.DragFloat(friendlyName + IDText(ID), ref floatVal, velocity, min, max)) + { + field.SetValue(config, floatVal); + + TriggerChangeEvent(config, field.Name, floatVal); + + return true; + } + + return false; + } + } + + [AttributeUsage(AttributeTargets.Field)] + public class DragIntAttribute : ConfigAttribute + { + public int min; + public int max; + public float velocity; + + public DragIntAttribute(string friendlyName) : base(friendlyName) + { + min = 1; + max = 1000; + velocity = 1; + } + + public override bool DrawField(FieldInfo field, PluginConfigObject config, string? ID, bool collapsingHeader) + { + int? fieldVal = (int?)field.GetValue(config); + int intVal = fieldVal.HasValue ? fieldVal.Value : 0; + + if (ImGui.DragInt(friendlyName + IDText(ID), ref intVal, velocity, min, max)) + { + field.SetValue(config, intVal); + + TriggerChangeEvent(config, field.Name, intVal); + + return true; + } + + return false; + } + } + + [AttributeUsage(AttributeTargets.Field)] + public class DragFloat2Attribute : ConfigAttribute + { + public float min; + public float max; + public float velocity; + + public DragFloat2Attribute(string friendlyName) : base(friendlyName) + { + min = 1f; + max = 1000f; + velocity = 1f; + } + + public override bool DrawField(FieldInfo field, PluginConfigObject config, string? ID, bool collapsingHeader) + { + Vector2? fieldVal = (Vector2?)field.GetValue(config); + Vector2 vectorVal = fieldVal.HasValue ? fieldVal.Value : Vector2.Zero; + + if (ImGui.DragFloat2(friendlyName + IDText(ID), ref vectorVal, velocity, min, max)) + { + field.SetValue(config, vectorVal); + + TriggerChangeEvent(config, field.Name, vectorVal); + + return true; + } + + return false; + } + } + + [AttributeUsage(AttributeTargets.Field)] + public class DragInt2Attribute : ConfigAttribute + { + public int min; + public int max; + public int velocity; + + public DragInt2Attribute(string friendlyName) : base(friendlyName) + { + min = 1; + max = 1000; + velocity = 1; + } + + public override bool DrawField(FieldInfo field, PluginConfigObject config, string? ID, bool collapsingHeader) + { + Vector2? fieldVal = (Vector2?)field.GetValue(config); + Vector2 vectorVal = fieldVal.HasValue ? fieldVal.Value : Vector2.Zero; + + if (ImGui.DragFloat2(friendlyName + IDText(ID), ref vectorVal, velocity, min, max)) + { + field.SetValue(config, vectorVal); + + TriggerChangeEvent(config, field.Name, vectorVal); + + return true; + } + + return false; + } + } + + [AttributeUsage(AttributeTargets.Field)] + public class InputTextAttribute : ConfigAttribute + { + public int maxLength; + public bool formattable = true; + + private string _searchText = ""; + + public InputTextAttribute(string friendlyName) : base(friendlyName) + { + this.friendlyName = friendlyName; + maxLength = 999; + } + + public override bool DrawField(FieldInfo field, PluginConfigObject config, string? ID, bool collapsingHeader) + { + string? fieldVal = (string?)field.GetValue(config); + string stringVal = fieldVal ?? ""; + string? finalValue = null; + + string popupId = ID != null ? "DelvUI_TextTagsList " + ID : "DelvUI_TextTagsList ##" + friendlyName; + + if (!formattable) + { + if (ImGui.InputText(friendlyName + IDText(ID), ref stringVal)) + { + finalValue = stringVal; + } + } + else + { + float scale = ImGuiHelpers.GlobalScale; + float width = ImGui.CalcItemWidth(); + float height = Math.Max(24 * scale, ImGui.CalcTextSize(stringVal, false, width).Y + 6 * scale); + Vector2 size = new Vector2(width, height); + + if (ImGui.InputTextMultiline(friendlyName + IDText(ID), ref stringVal, maxLength, size, ImGuiInputTextFlags.AllowTabInput)) + { + finalValue = stringVal; + } + + // text tags + ImGui.SameLine(); + ImGui.PushFont(UiBuilder.IconFont); + if (ImGui.Button(FontAwesomeIcon.Pen.ToIconString() + "##" + ID)) + { + ImGui.OpenPopup(popupId); + } + ImGui.PopFont(); + + ImGuiHelper.SetTooltip("Text Tags"); + } + + var selectedTag = ImGuiHelper.DrawTextTagsList(popupId, ref _searchText); + if (selectedTag != null) + { + finalValue = stringVal + selectedTag; + } + + if (finalValue != null) + { + field.SetValue(config, finalValue); + TriggerChangeEvent(config, field.Name, finalValue); + + return true; + } + + return false; + } + } + + [AttributeUsage(AttributeTargets.Field)] + public class ColorEdit4Attribute : ConfigAttribute + { + public ColorEdit4Attribute(string friendlyName) : base(friendlyName) { } + + public override bool DrawField(FieldInfo field, PluginConfigObject config, string? ID, bool collapsingHeader) + { + PluginConfigColor? colorVal = (PluginConfigColor?)field.GetValue(config); + Vector4 vector = (colorVal != null ? colorVal.Vector : Vector4.Zero); + + if (ImGui.ColorEdit4(friendlyName + IDText(ID), ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar)) + { + if (colorVal is null) + { + return false; + } + + colorVal.Vector = vector; + field.SetValue(config, colorVal); + + TriggerChangeEvent(config, field.Name, colorVal); + + return true; + } + + return false; + } + } + + [AttributeUsage(AttributeTargets.Field)] + public class ComboAttribute : ConfigAttribute + { + public string[] options; + + public ComboAttribute(string friendlyName, params string[] options) : base(friendlyName) + { + this.options = options; + } + + public override bool DrawField(FieldInfo field, PluginConfigObject config, string? ID, bool collapsingHeader) + { + object? fieldVal = field.GetValue(config); + + int intVal = 0; + if (fieldVal != null) + { + intVal = (int)fieldVal; + } + + if (ImGui.Combo(friendlyName + IDText(ID), ref intVal, options, 4)) + { + field.SetValue(config, intVal); + + TriggerChangeEvent(config, field.Name, intVal); + + return true; + } + + return false; + } + } + + [AttributeUsage(AttributeTargets.Field)] + public class DragDropHorizontalAttribute : ConfigAttribute + { + public string[] names; + + public DragDropHorizontalAttribute(string friendlyName, params string[] names) : base(friendlyName) + { + this.names = names; + } + + public override bool DrawField(FieldInfo field, PluginConfigObject config, string? ID, bool collapsingHeader) + { + ImGui.Text(friendlyName); + int[]? fieldVal = (int[]?)field.GetValue(config); + int[] order = fieldVal ?? Array.Empty(); + + for (int i = 0; i < order.Length; i++) + { + ImGui.SameLine(); + ImGui.Button(names[order[i]], new Vector2(100, 25)); + + if (ImGui.IsItemActive()) + { + float drag_dx = ImGui.GetMouseDragDelta(ImGuiMouseButton.Left).X; + + if ((drag_dx > 80.0f && i < order.Length - 1)) + { + var _curri = order[i]; + order[i] = order[i + 1]; + order[i + 1] = _curri; + field.SetValue(config, order); + ImGui.ResetMouseDragDelta(); + } + else if ((drag_dx < -80.0f && i > 0)) + { + var _curri = order[i]; + order[i] = order[i - 1]; + order[i - 1] = _curri; + field.SetValue(config, order); + ImGui.ResetMouseDragDelta(); + } + + TriggerChangeEvent(config, field.Name, order); + + return true; + } + } + + return false; + } + } + + [AttributeUsage(AttributeTargets.Field)] + public class FontAttribute : ConfigAttribute + { + public FontAttribute(string friendlyName = "Font and Size") : base(friendlyName) { } + + public override bool DrawField(FieldInfo field, PluginConfigObject config, string? ID, bool collapsingHeader) + { + var fontsConfig = ConfigurationManager.Instance.GetConfigObject(); + if (fontsConfig == null) + { + return false; + } + + string? stringVal = (string?)field.GetValue(config); + + int index = stringVal == null || stringVal.Length == 0 || !fontsConfig.Fonts.ContainsKey(stringVal) ? -1 : + fontsConfig.Fonts.IndexOfKey(stringVal); + + if (index == -1) + { + if (fontsConfig.Fonts.ContainsKey(FontsConfig.DefaultBigFontKey)) + { + index = fontsConfig.Fonts.IndexOfKey(FontsConfig.DefaultBigFontKey); + } + else + { + index = 0; + } + } + + var options = fontsConfig.Fonts.Values.Select(fontData => fontData.Name + " " + fontData.Size.ToString()).ToArray(); + + if (ImGui.Combo(friendlyName + IDText(ID), ref index, options, 4)) + { + stringVal = fontsConfig.Fonts.Keys[index]; + field.SetValue(config, stringVal); + + TriggerChangeEvent(config, field.Name, stringVal); + + return true; + } + + return false; + } + } + + [AttributeUsage(AttributeTargets.Field)] + public class BarTextureAttribute : ConfigAttribute + { + public BarTextureAttribute(string friendlyName = "Bar Texture") : base(friendlyName) { } + + public override bool DrawField(FieldInfo field, PluginConfigObject config, string? ID, bool collapsingHeader) + { + if (BarTexturesManager.Instance == null) + { + return false; + } + + List textures = BarTexturesManager.Instance.BarTextureNames.ToList(); + string? stringVal = (string?)field.GetValue(config); + + int index = 0; + if (stringVal != null && stringVal.Length > 0 && textures.Contains(stringVal)) + { + index = textures.IndexOf(stringVal); + } + + string[] options = textures.ToArray(); + + if (ImGui.Combo(friendlyName + IDText(ID), ref index, options, 10)) + { + stringVal = options[index]; + field.SetValue(config, stringVal); + + TriggerChangeEvent(config, field.Name, stringVal); + + return true; + } + + return false; + } + } + + [AttributeUsage(AttributeTargets.Field)] + public class AnchorAttribute : ComboAttribute + { + public AnchorAttribute(string friendlyName) + : base(friendlyName, new string[] { "Center", "Left", "Right", "Top", "TopLeft", "TopRight", "Bottom", "BottomLeft", "BottomRight" }) + { + } + } + + [AttributeUsage(AttributeTargets.Field)] + public class BarTextureDrawModeAttribute : ComboAttribute + { + public BarTextureDrawModeAttribute(string friendlyName) + : base(friendlyName, new string[] { "Stretch", "Repeat Horizontal", "Repeat Vertical", "Repeat" }) + { + } + } + + [AttributeUsage(AttributeTargets.Field)] + public class StrataLevelAttribute : ConfigAttribute + { + private string[] options = { "Lowest", "Low", "Mid-Low", "Mid", "Mid-High", "High", "Highest" }; + + public StrataLevelAttribute(string friendlyName) : base(friendlyName) + { + } + + public override bool DrawField(FieldInfo field, PluginConfigObject config, string? ID, bool collapsingHeader) + { + object? fieldVal = field.GetValue(config); + + int intVal = 0; + if (fieldVal != null) + { + intVal = (int)fieldVal; + } + + if (ImGui.Combo(friendlyName + IDText(ID), ref intVal, options, 4)) + { + field.SetValue(config, (StrataLevel?)intVal); + + TriggerChangeEvent(config, field.Name, intVal); + ConfigurationManager.Instance?.OnStrataLevelChanged(config); + + return true; + } + + return false; + } + } + #endregion + + #region field ordering attributes + [AttributeUsage(AttributeTargets.Field)] + public class OrderAttribute : Attribute + { + public int pos; + public string? collapseWith = "Enabled"; + + public OrderAttribute(int pos) + { + this.pos = pos; + } + } + + [AttributeUsage(AttributeTargets.Field)] + public class NestedConfigAttribute : OrderAttribute + { + public string friendlyName; + public bool separator = false; + public bool spacing = true; + public bool nest = true; + public bool collapsingHeader = true; + + public NestedConfigAttribute(string friendlyName, int pos) : base(pos) + { + this.friendlyName = friendlyName; + + } + } + + #endregion +} diff --git a/Config/Attributes/SectionAttributes.cs b/Config/Attributes/SectionAttributes.cs new file mode 100644 index 0000000..85471f7 --- /dev/null +++ b/Config/Attributes/SectionAttributes.cs @@ -0,0 +1,30 @@ +using System; + +namespace HSUI.Config.Attributes +{ + [AttributeUsage(AttributeTargets.Class)] + public class SectionAttribute : Attribute + { + public string SectionName; + public bool ForceAllowExport; + + public SectionAttribute(string name, bool forceAllowExport = false) + { + SectionName = name; + ForceAllowExport = forceAllowExport; + } + } + + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public class SubSectionAttribute : Attribute + { + public string SubSectionName; + public int Depth; + + public SubSectionAttribute(string subSectionName, int depth) + { + SubSectionName = subSectionName; + Depth = depth; + } + } +} diff --git a/Config/ConfigurationManager.cs b/Config/ConfigurationManager.cs new file mode 100644 index 0000000..f871615 --- /dev/null +++ b/Config/ConfigurationManager.cs @@ -0,0 +1,703 @@ +using Dalamud.Interface.Internal; +using Dalamud.Interface.Windowing; +using Dalamud.Logging; +using HSUI.Config.Profiles; +using HSUI.Config.Tree; +using HSUI.Config.Windows; +using HSUI.Helpers; +using HSUI.Interface; +using HSUI.Interface.EnemyList; +using HSUI.Interface.GeneralElements; +using HSUI.Interface.Jobs; +using HSUI.Interface.Party; +using HSUI.Interface.PartyCooldowns; +using HSUI.Interface.StatusEffects; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace HSUI.Config +{ + public delegate void ConfigurationManagerEventHandler(ConfigurationManager configurationManager); + public delegate void StrataLevelsEventHandler(ConfigurationManager configurationManager, PluginConfigObject config); + public delegate void GlobalVisibilityEventHandler(ConfigurationManager configurationManager, VisibilityConfig config); + + public class ConfigurationManager : IDisposable + { + public static ConfigurationManager Instance { get; private set; } = null!; + + private BaseNode _configBaseNode; + private Dictionary _configBaseNodeByProfile; + public BaseNode ConfigBaseNode + { + get => _configBaseNode; + set + { + _configBaseNode = value; + _mainConfigWindow.node = value; + } + } + + private WindowSystem _windowSystem; + private MainConfigWindow _mainConfigWindow; + private ChangelogWindow _changelogWindow; + private GridWindow _gridWindow; + + public bool IsConfigWindowOpened => _mainConfigWindow.IsOpen; + public bool IsChangelogWindowOpened => _changelogWindow.IsOpen; + public bool ShowingModalWindow = false; + + public GradientDirection GradientDirection + { + get + { + var config = Instance.GetConfigObject(); + return config != null ? config.GradientDirection : GradientDirection.None; + } + } + + public bool OverrideDalamudStyle + { + get + { + HUDOptionsConfig config = Instance.GetConfigObject(); + return config != null ? config.OverrideDalamudStyle : true; + } + } + + public CultureInfo ActiveCultreInfo + { + get { + HUDOptionsConfig config = Instance.GetConfigObject(); + return config == null || config.UseRegionalNumberFormats ? CultureInfo.CurrentCulture : CultureInfo.InvariantCulture; + } + } + + public string ConfigDirectory; + + public string CurrentVersion => Plugin.Version; + public string? PreviousVersion { get; private set; } = null; + + private bool _needsProfileUpdate = false; + private bool _lockHUD = true; + + public bool LockHUD + { + get => _lockHUD; + set + { + if (_lockHUD == value) + { + return; + } + + _lockHUD = value; + _mainConfigWindow.IsOpen = value; + _gridWindow.IsOpen = !value; + + LockEvent?.Invoke(this); + + if (_lockHUD) + { + ConfigBaseNode.NeedsSave = true; + } + } + } + + public bool ShowHUD = true; + + public event ConfigurationManagerEventHandler? ResetEvent; + public event ConfigurationManagerEventHandler? LockEvent; + public event ConfigurationManagerEventHandler? ConfigClosedEvent; + public event StrataLevelsEventHandler? StrataLevelsChangedEvent; + public event GlobalVisibilityEventHandler? GlobalVisibilityEvent; + + public ConfigurationManager() + { + ConfigDirectory = Plugin.PluginInterface.GetPluginConfigDirectory(); + + _configBaseNodeByProfile = new Dictionary(); + _configBaseNode = new BaseNode(); + InitializeBaseNode(_configBaseNode); + _configBaseNode.ConfigObjectResetEvent += OnConfigObjectReset; + + _mainConfigWindow = new MainConfigWindow("HSUI Settings"); + _mainConfigWindow.node = _configBaseNode; + _mainConfigWindow.CloseAction = () => + { + ConfigClosedEvent?.Invoke(this); + + if (ConfigBaseNode.NeedsSave) + { + SaveConfigurations(); + } + + if (_needsProfileUpdate) + { + UpdateCurrentProfile(); + _needsProfileUpdate = false; + } + }; + + string changelog = LoadChangelog(); + _changelogWindow = new ChangelogWindow("HSUI Changelog v" + Plugin.Version, changelog); + _gridWindow = new GridWindow("Grid ##HSUI"); + + _windowSystem = new WindowSystem("HSUI_Windows"); + _windowSystem.AddWindow(_mainConfigWindow); + _windowSystem.AddWindow(_changelogWindow); + _windowSystem.AddWindow(_gridWindow); + + CheckVersion(); + + Plugin.ClientState.Logout += OnLogout; + Plugin.JobChangedEvent += OnJobChanged; + + _configBaseNode.CreateNodesIfNeeded(); + } + + ~ConfigurationManager() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + + ConfigBaseNode.ConfigObjectResetEvent -= OnConfigObjectReset; + Plugin.ClientState.Logout -= OnLogout; + Plugin.JobChangedEvent -= OnJobChanged; + + Instance = null!; + } + + public static void Initialize() { Instance = new ConfigurationManager(); } + + private void OnConfigObjectReset(BaseNode sender) + { + ResetEvent?.Invoke(this); + } + + private void OnLogout(int type, int code) + { + SaveConfigurations(); + ProfilesManager.Instance?.SaveCurrentProfile(); + } + + private void OnJobChanged(uint jobId) + { + UpdateCurrentProfile(); + } + + private string LoadChangelog() + { + string path = Path.Combine(Plugin.AssemblyLocation, "changelog.md"); + + try + { + string fullChangelog = File.ReadAllText(path); + string versionChangelog = fullChangelog.Split("#", StringSplitOptions.RemoveEmptyEntries)[0]; + return versionChangelog.Replace(Plugin.Version, ""); + } + catch (Exception e) + { + Plugin.Logger.Error("Error loading changelog: " + e.Message); + } + + return ""; + } + + private void CheckVersion() + { + string path = Path.Combine(ConfigDirectory, "version"); + + bool needsBackup = false; + try + { + bool needsWrite = false; + + if (!File.Exists(path)) + { + needsWrite = true; + } + else + { + PreviousVersion = File.ReadAllText(path); + if (PreviousVersion != Plugin.Version) + { + needsWrite = true; + needsBackup = true; + } + } + + _changelogWindow.IsOpen = needsWrite; + _changelogWindow.AutoClose = true; + + if (needsWrite) + { + File.WriteAllText(path, Plugin.Version); + } + } + catch (Exception e) + { + Plugin.Logger.Error("Error checking version: " + e.Message); + } + + try + { + if (needsBackup && PreviousVersion != null) + { + BackupFiles(PreviousVersion); + } + } + catch (Exception e) + { + Plugin.Logger.Error("Error making backup: " + e.Message); + } + } + + private void BackupFiles(string version) + { + string backupsRoot = Path.Combine(ConfigDirectory, "Backups"); + if (!Directory.Exists(backupsRoot)) + { + Directory.CreateDirectory(backupsRoot); + } + + string backupPath = Path.Combine(backupsRoot, version); + + foreach (string folderPath in Directory.GetDirectories(ConfigDirectory, "*", SearchOption.AllDirectories)) + { + if (folderPath.Contains("Backups")) { continue; } + + Directory.CreateDirectory(folderPath.Replace(ConfigDirectory, backupPath)); + } + + foreach (string filePath in Directory.GetFiles(ConfigDirectory, "*.*", SearchOption.AllDirectories)) + { + File.Copy(filePath, filePath.Replace(ConfigDirectory, backupPath), true); + } + } + + #region strata + public void OnStrataLevelChanged(PluginConfigObject config) + { + StrataLevelsChangedEvent?.Invoke(this, config); + } + #endregion + + #region visibility + public void OnGlobalVisibilityChanged(VisibilityConfig config) + { + GlobalVisibilityEvent?.Invoke(this, config); + } + #endregion + + #region windows + public void ToggleConfigWindow() + { + _mainConfigWindow.Toggle(); + } + + public void OpenConfigWindow() + { + _mainConfigWindow.IsOpen = true; + } + + public void CloseConfigWindow() + { + _mainConfigWindow.IsOpen = false; + } + + public void OpenChangelogWindow() + { + _changelogWindow.IsOpen = true; + } + + public void Draw() + { + _windowSystem.Draw(); + } + + public void AddExtraSectionNode(SectionNode node) + { + ConfigBaseNode.AddExtraSectionNode(node); + } + #endregion + + #region config getters and setters + public PluginConfigObject GetConfigObjectForType(Type type) + { + MethodInfo? genericMethod = GetType().GetMethod("GetConfigObject"); + MethodInfo? method = genericMethod?.MakeGenericMethod(type); + return (PluginConfigObject)method?.Invoke(this, null)!; + } + public T GetConfigObject() where T : PluginConfigObject => ConfigBaseNode.GetConfigObject()!; + + public static PluginConfigObject GetDefaultConfigObjectForType(Type type) + { + MethodInfo? method = type.GetMethod("DefaultConfig", BindingFlags.Public | BindingFlags.Static); + return (PluginConfigObject)method?.Invoke(null, null)!; + } + + public ConfigPageNode GetConfigPageNode() where T : PluginConfigObject => ConfigBaseNode.GetConfigPageNode()!; + + public void SetConfigObject(PluginConfigObject configObject) => ConfigBaseNode.SetConfigObject(configObject); + + public List GetObjects() => ConfigBaseNode.GetObjects(); + #endregion + + #region load / save / profiles + public bool IsFreshInstall() + { + return Directory.GetDirectories(ConfigDirectory).Length == 0; + } + + public void LoadOrInitializeFiles() + { + try + { + if (!IsFreshInstall()) + { + LoadConfigurations(); + + // gotta save after initial load store possible version update changes right away + SaveConfigurations(true); + } + } + catch (Exception e) + { + Plugin.Logger.Error("Error initializing configurations: " + e.Message); + + if (e.StackTrace != null) + { + Plugin.Logger.Error(e.StackTrace); + } + } + } + + public void ForceNeedsSave() + { + ConfigBaseNode.NeedsSave = true; + } + + public void LoadConfigurations() + { + PerformV2Migration(); + ConfigBaseNode.Load(ConfigDirectory); + } + + public void SaveConfigurations(bool forced = false) + { + if (!forced && !ConfigBaseNode.NeedsSave) + { + return; + } + + ConfigBaseNode.Save(ConfigDirectory); + + ProfilesManager.Instance?.SaveCurrentProfile(); + + ConfigBaseNode.NeedsSave = false; + } + + public void PerformV2Migration() + { + // create necessary folders + string[] newFolders = new string[] { "Other Elements", "Customization" }; + foreach (string folder in newFolders) + { + string path = Path.Combine(ConfigDirectory, folder); + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + } + + // move files + Dictionary files = new Dictionary() + { + ["Misc\\Experience Bar.json"] = "Other Elements\\Experience Bar.json", + ["Misc\\GCD Indicator.json"] = "Other Elements\\GCD Indicator.json", + ["Misc\\Limit Break.json"] = "Other Elements\\Limit Break.json", + ["Misc\\MP Ticker.json"] = "Other Elements\\MP Ticker.json", + ["Misc\\Pull Timer.json"] = "Other Elements\\Pull Timer.json", + ["Misc\\Fonts.json"] = "Customization\\Fonts.json" + }; + + foreach (string key in files.Keys) + { + string v1Path = Path.Combine(ConfigDirectory, key); + string v2Path = Path.Combine(ConfigDirectory, files[key]); + + try + { + if (File.Exists(v1Path) && !File.Exists(v2Path)) + { + File.Move(v1Path, v2Path); + } + } + catch (Exception e) + { + Plugin.Logger.Error("Error migrating file \"" + v1Path + "\" to v2 config structure: " + e.Message); + } + } + } + + public void UpdateCurrentProfile() + { + // dont update the profile on job change when the config window is opened + if (_mainConfigWindow.IsOpen) + { + _needsProfileUpdate = true; + return; + } + + ProfilesManager.Instance?.UpdateCurrentProfile(); + } + + public string? ExportCurrentConfigs() + { + return ConfigBaseNode.GetBase64String(); + } + + public void OnProfileDeleted(string profileName) + { + try + { + _configBaseNodeByProfile.Remove(profileName); + } + catch { } + } + + public bool ImportProfile(string oldProfileName, string profileName, string rawString, bool forceLoad = false) + { + // cache old profile + _configBaseNodeByProfile[oldProfileName] = ConfigBaseNode; + + // load profile from cache or from rawString + BaseNode? loadedNode = null; + if (forceLoad || !_configBaseNodeByProfile.TryGetValue(profileName, out loadedNode)) + { + ImportProfileNonCached(rawString, out loadedNode); + } + + if (loadedNode == null) { return false; } + + if (IsConfigWindowOpened || string.IsNullOrEmpty(loadedNode.SelectedOptionName)) + { + loadedNode.SelectedOptionName = ConfigBaseNode.SelectedOptionName; + loadedNode.RefreshSelectedNode(); + } + + ConfigBaseNode.ConfigObjectResetEvent -= OnConfigObjectReset; + ConfigBaseNode = loadedNode; + ConfigBaseNode.ConfigObjectResetEvent += OnConfigObjectReset; + + PerformV2Migration(); + + ResetEvent?.Invoke(this); + + return true; + } + + private bool ImportProfileNonCached(string rawString, out BaseNode? node) + { + List importStrings = new List(rawString.Trim().Split(new string[] { "|" }, StringSplitOptions.RemoveEmptyEntries)); + ImportData[] imports = importStrings.Select(str => new ImportData(str)).ToArray(); + + node = new BaseNode(); + InitializeBaseNode(node); + + Dictionary oldConfigObjects = new Dictionary(); + + foreach (ImportData importData in imports) + { + PluginConfigObject? config = importData.GetObject(); + if (config == null) + { + return false; + } + + if (!node.SetConfigObject(config)) + { + oldConfigObjects.Add(config.GetType(), config); + } + } + + if (ProfilesManager.Instance != null) + { + node.AddExtraSectionNode(ProfilesManager.Instance.ProfilesNode); + } + + return true; + } + #endregion + + #region initialization + private static void InitializeBaseNode(BaseNode node) + { + // creates node tree in the right order... + foreach (Type type in ConfigObjectTypes) + { + var genericMethod = node.GetType().GetMethod("GetConfigPageNode"); + var method = genericMethod?.MakeGenericMethod(type); + method?.Invoke(node, null); + } + } + + private static Type[] ConfigObjectTypes = new Type[] + { + // Unit Frames + typeof(PlayerUnitFrameConfig), + typeof(TargetUnitFrameConfig), + typeof(TargetOfTargetUnitFrameConfig), + typeof(FocusTargetUnitFrameConfig), + + // Mana Bars + typeof(PlayerPrimaryResourceConfig), + typeof(TargetPrimaryResourceConfig), + typeof(TargetOfTargetPrimaryResourceConfig), + typeof(FocusTargetPrimaryResourceConfig), + + // Castbars + typeof(PlayerCastbarConfig), + typeof(TargetCastbarConfig), + typeof(TargetOfTargetCastbarConfig), + typeof(FocusTargetCastbarConfig), + + // Buffs and Debuffs + typeof(PlayerBuffsListConfig), + typeof(PlayerDebuffsListConfig), + typeof(TargetBuffsListConfig), + typeof(TargetDebuffsListConfig), + typeof(FocusTargetBuffsListConfig), + typeof(FocusTargetDebuffsListConfig), + typeof(CustomEffectsListConfig), + + // Nameplates + typeof(NameplatesGeneralConfig), + typeof(PlayerNameplateConfig), + typeof(EnemyNameplateConfig), + typeof(PartyMembersNameplateConfig), + typeof(AllianceMembersNameplateConfig), + typeof(FriendPlayerNameplateConfig), + typeof(OtherPlayerNameplateConfig), + typeof(PetNameplateConfig), + typeof(NPCNameplateConfig), + typeof(MinionNPCNameplateConfig), + typeof(ObjectsNameplateConfig), + + // Party Frames + typeof(PartyFramesConfig), + typeof(PartyFramesHealthBarsConfig), + typeof(PartyFramesManaBarConfig), + typeof(PartyFramesCastbarConfig), + typeof(PartyFramesIconsConfig), + typeof(PartyFramesBuffsConfig), + typeof(PartyFramesDebuffsConfig), + typeof(PartyFramesTrackersConfig), + typeof(PartyFramesCooldownListConfig), + + // Party Cooldowns + typeof(PartyCooldownsConfig), + typeof(PartyCooldownsBarConfig), + typeof(PartyCooldownsDataConfig), + + // Enemy List + typeof(EnemyListConfig), + typeof(EnemyListHealthBarConfig), + typeof(EnemyListEnmityIconConfig), + typeof(EnemyListSignIconConfig), + typeof(EnemyListCastbarConfig), + typeof(EnemyListBuffsConfig), + typeof(EnemyListDebuffsConfig), + + // Job Specific Bars + typeof(PaladinConfig), + typeof(WarriorConfig), + typeof(DarkKnightConfig), + typeof(GunbreakerConfig), + + typeof(WhiteMageConfig), + typeof(ScholarConfig), + typeof(AstrologianConfig), + typeof(SageConfig), + + typeof(MonkConfig), + typeof(DragoonConfig), + typeof(NinjaConfig), + typeof(SamuraiConfig), + typeof(ReaperConfig), + typeof(ViperConfig), + + typeof(BardConfig), + typeof(MachinistConfig), + typeof(DancerConfig), + + typeof(BlackMageConfig), + typeof(SummonerConfig), + typeof(RedMageConfig), + typeof(BlueMageConfig), + typeof(PictomancerConfig), + + // Other Elements + typeof(ExperienceBarConfig), + typeof(GCDIndicatorConfig), + typeof(HotbarsConfig), + typeof(Hotbar1BarConfig), + typeof(Hotbar2BarConfig), + typeof(Hotbar3BarConfig), + typeof(Hotbar4BarConfig), + typeof(Hotbar5BarConfig), + typeof(Hotbar6BarConfig), + typeof(Hotbar7BarConfig), + typeof(Hotbar8BarConfig), + typeof(Hotbar9BarConfig), + typeof(Hotbar10BarConfig), + typeof(PullTimerConfig), + typeof(LimitBreakConfig), + typeof(MPTickerConfig), + + // Colors + typeof(TanksColorConfig), + typeof(HealersColorConfig), + typeof(MeleeColorConfig), + typeof(RangedColorConfig), + typeof(CastersColorConfig), + typeof(RolesColorConfig), + typeof(MiscColorConfig), + + // Customization + typeof(FontsConfig), + typeof(BarTexturesConfig), + + // Visibility + typeof(GlobalVisibilityConfig), + typeof(HotbarsVisibilityConfig), + + // Misc + typeof(HUDOptionsConfig), + typeof(WindowClippingConfig), + typeof(TooltipsConfig), + typeof(GridConfig), + + // Import + typeof(ImportConfig) + }; + #endregion + } +} \ No newline at end of file diff --git a/Config/ImportConfig.cs b/Config/ImportConfig.cs new file mode 100644 index 0000000..a2a737a --- /dev/null +++ b/Config/ImportConfig.cs @@ -0,0 +1,301 @@ +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Config.Profiles; +using HSUI.Helpers; +using Dalamud.Bindings.ImGui; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Reflection; +using System.Text.RegularExpressions; + +namespace HSUI.Interface +{ + [Disableable(false)] + [Exportable(false)] + [Shareable(false)] + [Resettable(false)] + [Section("Import")] + [SubSection("General", 0)] + public class ImportConfig : PluginConfigObject + { + private string _importString = ""; + private bool _importing = false; + private string? _errorMessage = null; + + private List? _importDataList = null; + private List? _importDataEnabled = null; + + public new static ImportConfig DefaultConfig() { return new ImportConfig(); } + + [ManualDraw] + public bool Draw(ref bool changed) + { + ImGui.Text("Import string:"); + + ImGui.InputText("", ref _importString, 999999); + + ImGui.Text("Here you can import specific parts of a profile.\nIf the string contains more than one part you will be able to select which parts you wish to import."); + + if (ImGui.Button("Import", new Vector2(560, 24))) + { + _importing = _importString.Length > 0; + } + + ImGuiHelper.DrawSeparator(1, 1); + ImGui.Text("To browse presets made by users of the HSUI community join our Discord and find the #profiles channel."); + + if (ImGui.Button("HSUI Discord", new Vector2(560, 24))) + { + Utils.OpenUrl("https://discord.gg/xzde5qQayh"); + } + + // error modal + if (_errorMessage != null) + { + if (ImGuiHelper.DrawErrorModal(_errorMessage)) + { + _importing = false; + _errorMessage = null; + } + + return false; + } + + // parse import string + if (_importing && _importDataList == null) + { + _errorMessage = Parse(); + } + + // confirmation modal + if (_importDataList != null && _importDataList.Count > 0) + { + var (didConfirm, didClose) = DrawImportConfirmationModal(); + + if (didConfirm) + { + _errorMessage = Import(); + + if (_errorMessage == null) + { + _importString = ""; + } + } + + if (didConfirm || didClose) + { + _importing = false; + _importDataList = null; + _importDataEnabled = null; + changed = true; + } + + return didConfirm && _errorMessage == null; + } + + return false; + } + + private string? Import() + { + if (_importDataList == null || _importDataEnabled == null) + { + return null; + } + + List configObjects = new List(_importDataList.Count); + + for (int i = 0; i < _importDataList.Count; i++) + { + if (i >= _importDataEnabled.Count || _importDataEnabled[i] == false) + { + continue; + } + + ImportData importData = _importDataList[i]; + PluginConfigObject? config = importData.GetObject(); + if (config == null) + { + return "Couldn't import \"" + importData.Name + "\""; + } + + configObjects.Add(config); + } + + foreach (PluginConfigObject config in configObjects) + { + ConfigurationManager.Instance.SetConfigObject(config); + } + + return null; + } + + private string? Parse() + { + string[] importStrings = _importString.Trim().Split(new string[] { "|" }, StringSplitOptions.RemoveEmptyEntries); + if (importStrings.Length == 0) + { + return null; + } + + _importDataList = new List(importStrings.Length); + _importDataEnabled = new List(importStrings.Length); + + foreach (var str in importStrings) + { + try + { + ImportData importData = new ImportData(str); + _importDataList.Add(importData); + _importDataEnabled.Add(true); + } + catch (Exception e) + { + _importDataList = null; + _importDataEnabled = null; + + return e is ArgumentException ? e.Message : "Invalid import string!"; + } + } + + return null; + } + + public (bool, bool) DrawImportConfirmationModal() + { + if (_importDataList == null || _importDataEnabled == null) + { + return (false, true); + } + + ConfigurationManager.Instance.ShowingModalWindow = true; + + bool didConfirm = false; + bool didClose = false; + + ImGui.OpenPopup("Import ##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("Import ##HSUI", ref p_open, ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoMove)) + { + float width = 300; + + ImGui.Text("Select which parts to import:"); + + ImGui.NewLine(); + if (ImGui.Button("Select All", new Vector2(width / 2f - 5, 24))) + { + for (int i = 0; i < _importDataEnabled.Count; i++) + { + _importDataEnabled[i] = true; + } + } + + ImGui.SameLine(); + if (ImGui.Button("Deselect All", new Vector2(width / 2f - 5, 24))) + { + for (int i = 0; i < _importDataEnabled.Count; i++) + { + _importDataEnabled[i] = false; + } + } + + ImGui.NewLine(); + float height = Math.Min(30 * _importDataList.Count, 400); + + ImGui.BeginChild("import checkboxes", new Vector2(width, height), false); + + for (int i = 0; i < _importDataList.Count; i++) + { + bool value = _importDataEnabled[i]; + if (ImGui.Checkbox(_importDataList[i].Name, ref value)) + { + _importDataEnabled[i] = value; + } + } + + 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 class ImportData + { + public readonly Type ConfigType; + public readonly string Name; + + public readonly string ImportString; + public readonly string JsonString; + + public ImportData(string base64String) + { + ImportString = base64String; + JsonString = ImportExportHelper.Base64DecodeAndDecompress(base64String); + + string? typeString = (string?)JObject.Parse(JsonString)["$type"]; + if (typeString == null) + { + throw new ArgumentException("Invalid type"); + } + + // Migrate DelvUI type names to HSUI for profile/import compatibility + if (typeString.Contains("DelvUI")) + { + typeString = typeString.Replace("DelvUI.", "HSUI.").Replace(", DelvUI", ", HSUI"); + } + + Type? type = Type.GetType(typeString); + if (type == null) + { + throw new ArgumentException("Invalid type: \"" + typeString + "\""); + } + + ConfigType = type; + Name = Utils.UserFriendlyConfigName(type.Name); + } + + public PluginConfigObject? GetObject() + { + MethodInfo? methodInfo = typeof(PluginConfigObject).GetMethod("LoadFromJsonString", BindingFlags.Public | BindingFlags.Static); + MethodInfo? function = methodInfo?.MakeGenericMethod(ConfigType); + return (PluginConfigObject?)function?.Invoke(this, new object[] { JsonString })!; + } + } +} diff --git a/Config/OnChangeEventArgs.cs b/Config/OnChangeEventArgs.cs new file mode 100644 index 0000000..392fec4 --- /dev/null +++ b/Config/OnChangeEventArgs.cs @@ -0,0 +1,43 @@ +using System; + +namespace HSUI.Config +{ + public delegate void ConfigValueChangeEventHandler(PluginConfigObject sender, OnChangeBaseArgs args); + + public enum ChangeType + { + None = 0, + ListAdd = 1, + ListRemove = 2 + } + + public class OnChangeBaseArgs : EventArgs + { + public string PropertyName { get; } + public ChangeType ChangeType { get; private set; } + + public OnChangeBaseArgs(string keyName, ChangeType type = ChangeType.None) + { + PropertyName = keyName; + ChangeType = type; + } + } + + public class OnChangeEventArgs : OnChangeBaseArgs + { + public T Value { get; } + + public OnChangeEventArgs(string keyName, T value, ChangeType type = ChangeType.None) : base(keyName, type) + { + Value = value; + } + } + + public interface IOnChangeEventArgs + { + public abstract event ConfigValueChangeEventHandler? ValueChangeEvent; + + public abstract void OnValueChanged(OnChangeBaseArgs e); + } + +} diff --git a/Config/PluginConfigObject.cs b/Config/PluginConfigObject.cs new file mode 100644 index 0000000..c391f44 --- /dev/null +++ b/Config/PluginConfigObject.cs @@ -0,0 +1,272 @@ +using HSUI.Config.Attributes; +using HSUI.Enums; +using Dalamud.Bindings.ImGui; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Numerics; +using System.Reflection; + +namespace HSUI.Config +{ + public abstract class PluginConfigObject : IOnChangeEventArgs + { + public string Version => Plugin.Version; + + [Checkbox("Enabled")] + [Order(0, collapseWith = null)] + public bool Enabled = true; + + #region convenience properties + [JsonIgnore] + public bool Exportable + { + get + { + ExportableAttribute? attribute = (ExportableAttribute?)GetType().GetCustomAttribute(typeof(ExportableAttribute), false); + return attribute == null || attribute.exportable; + } + } + + [JsonIgnore] + public bool Shareable + { + get + { + ShareableAttribute? attribute = (ShareableAttribute?)GetType().GetCustomAttribute(typeof(ShareableAttribute), false); + return attribute == null || attribute.shareable; + } + } + + [JsonIgnore] + public bool Resettable + { + get + { + ResettableAttribute? attribute = (ResettableAttribute?)GetType().GetCustomAttribute(typeof(ResettableAttribute), false); + return attribute == null || attribute.resettable; + } + } + + [JsonIgnore] + public bool Disableable + { + get + { + DisableableAttribute? attribute = (DisableableAttribute?)GetType().GetCustomAttribute(typeof(DisableableAttribute), false); + return attribute == null || attribute.disableable; + } + } + + [JsonIgnore] + public string[]? DisableParentSettings + { + get + { + DisableParentSettingsAttribute? attribute = (DisableParentSettingsAttribute?)GetType().GetCustomAttribute(typeof(DisableParentSettingsAttribute), true); + return attribute?.DisabledFields; + } + } + #endregion + + protected bool ColorEdit4(string label, ref PluginConfigColor color) + { + var vector = color.Vector; + + if (ImGui.ColorEdit4(label, ref vector)) + { + color.Vector = vector; + + return true; + } + + return false; + } + + public static PluginConfigObject DefaultConfig() + { + return null!; + } + + public List GetObjects() + { + List list = new List(); + + Type type = typeof(T); + if (this is T obj) + { + list.Add(obj); + } + + // iterate properties + PropertyInfo[] properties = GetType().GetProperties(); + foreach (PropertyInfo property in properties) + { + object? value = property.GetValue(this); + + if (value is T o) + { + list.Add(o); + } + else if (value is PluginConfigObject p) + { + list.AddRange(p.GetObjects()); + } + } + + // iterate fields + FieldInfo[] fields = GetType().GetFields(); + foreach (FieldInfo field in fields) + { + object? value = field.GetValue(this); + + if (value is T o) + { + list.Add(o); + } + else if (value is PluginConfigObject p) + { + list.AddRange(p.GetObjects()); + } + } + + return list; + } + + public T? Load(FileInfo fileInfo) where T : PluginConfigObject + { + return LoadFromJson(fileInfo.FullName); + } + + public static T? LoadFromJson(string path) where T : PluginConfigObject + { + if (!File.Exists(path)) { return null; } + + return LoadFromJsonString(File.ReadAllText(path)); + } + + public static T? LoadFromJsonString(string jsonString) where T : PluginConfigObject + { + JsonSerializerSettings settings = new JsonSerializerSettings(); + settings.ContractResolver = new PluginConfigObjectsContractResolver(); + + return JsonConvert.DeserializeObject(jsonString, settings); + } + + #region IOnChangeEventArgs + + // sending event outside of the config + public event ConfigValueChangeEventHandler? ValueChangeEvent; + + // received events from the node + public void OnValueChanged(OnChangeBaseArgs e) + { + ValueChangeEvent?.Invoke(this, e); + } + + #endregion + } + + public abstract class MovablePluginConfigObject : PluginConfigObject + { + [JsonIgnore] + public string ID; + + [StrataLevel("Strata Level")] + [Order(2)] + public StrataLevel? Strata; + + public StrataLevel StrataLevel => Strata ?? StrataLevel.LOWEST; + + [DragInt2("Position", min = -4000, max = 4000)] + [Order(5)] + public Vector2 Position = Vector2.Zero; + + public MovablePluginConfigObject() + { + ID = $"DelvUI_{GetType().Name}_{Guid.NewGuid()}"; + } + } + + public abstract class AnchorablePluginConfigObject : MovablePluginConfigObject + { + [DragInt2("Size", min = 1, max = 4000, isMonitored = true)] + [Order(10)] + public Vector2 Size; + + [Anchor("Anchor")] + [Order(15)] + public DrawAnchor Anchor = DrawAnchor.Center; + } + + public class PluginConfigColor + { + [JsonIgnore] private float[] _colorMapRatios = { -.8f, -.3f, .1f }; + + [JsonIgnore] private Vector4 _vector; + + public PluginConfigColor(Vector4 vector, float[]? colorMapRatios = null) + { + _vector = vector; + + if (colorMapRatios != null && colorMapRatios.Length >= 3) + { + _colorMapRatios = colorMapRatios; + } + + Update(); + } + + public static PluginConfigColor FromHex(uint hexColor) + { + // ARGB to ABGR + uint r = (hexColor >> 16) & 0xFF; + uint b = hexColor & 0xFF; + hexColor = (hexColor & 0xFF00FF00) | (b << 16) | r; + + return new PluginConfigColor(ImGui.ColorConvertU32ToFloat4(hexColor)); + } + + public PluginConfigColor WithAlpha(float alpha) + { + if (alpha == Vector.W) { return this; } + + return new PluginConfigColor(Vector.WithNewAlpha(alpha)); + } + + public static PluginConfigColor Empty => new(Vector4.Zero); + + public Vector4 Vector + { + get => _vector; + set + { + if (_vector == value) + { + return; + } + + _vector = value; + + Update(); + } + } + + [JsonIgnore] public uint Base { get; private set; } + + [JsonIgnore] public uint Background { get; private set; } + + [JsonIgnore] public uint TopGradient { get; private set; } + + [JsonIgnore] public uint BottomGradient { get; private set; } + + private void Update() + { + Base = ImGui.ColorConvertFloat4ToU32(_vector); + Background = ImGui.ColorConvertFloat4ToU32(_vector.AdjustColor(_colorMapRatios[0])); + TopGradient = ImGui.ColorConvertFloat4ToU32(_vector.AdjustColor(_colorMapRatios[1])); + BottomGradient = ImGui.ColorConvertFloat4ToU32(_vector.AdjustColor(_colorMapRatios[2])); + } + } +} diff --git a/Config/PluginConfigObjectConverter.cs b/Config/PluginConfigObjectConverter.cs new file mode 100644 index 0000000..91e69f6 --- /dev/null +++ b/Config/PluginConfigObjectConverter.cs @@ -0,0 +1,310 @@ +using Dalamud.Logging; +using HSUI.Interface.EnemyList; +using HSUI.Interface.GeneralElements; +using HSUI.Interface.Party; +using HSUI.Interface.StatusEffects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.Serialization; + +namespace HSUI.Config +{ + public abstract class PluginConfigObjectConverter : JsonConverter + { + protected Dictionary FieldConvertersMap = new Dictionary(); + + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + var genericMethod = GetType().GetMethod("ConvertJson"); + var method = genericMethod?.MakeGenericMethod(objectType); + return method?.Invoke(this, new object[] { reader, serializer }); + } + + public T? ConvertJson(JsonReader reader, JsonSerializer serializer) where T : PluginConfigObject + { + Type type = typeof(T); + T? config = null; + + try + { + ConstructorInfo? constructor = type.GetConstructor(new Type[] { }); + if (constructor != null) + { + config = (T?)Activator.CreateInstance(); + } + else + { + config = (T?)ConfigurationManager.GetDefaultConfigObjectForType(type); + } + + // last resource, hackily create an instance without calling the constructor + if (config == null) + { +#pragma warning disable SYSLIB0050 // Type or member is obsolete + config = (T)FormatterServices.GetUninitializedObject(type); +#pragma warning restore SYSLIB0050 // Type or member is obsolete + } + } + catch (Exception e) + { + Plugin.Logger.Error($"Error creating a {type.Name}: " + e.Message); + } + + if (config == null) { return null; } + + try + { + JObject? jsonObject = (JObject?)serializer.Deserialize(reader); + if (jsonObject == null) { return null; } + + Dictionary ValuesMap = new Dictionary(); + + // get values from json + foreach (JProperty property in jsonObject.Properties()) + { + string propertyName = property.Name; + object? value = null; + + // convert values if needed + if (FieldConvertersMap.TryGetValue(propertyName, out PluginConfigObjectFieldConverter? fieldConverter) && fieldConverter != null) + { + (propertyName, value) = fieldConverter.Convert(property.Value); + } + // read value from json + else + { + FieldInfo? field = type.GetField(propertyName); + if (field != null) + { + value = property.Value.ToObject(field.FieldType); + } + } + + if (value != null) + { + ValuesMap.Add(propertyName, value); + } + } + + // apply values + foreach (string key in ValuesMap.Keys) + { + string[] fields = key.Split(".", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + object? currentObject = config; + object value = ValuesMap[key]; + + for (int i = 0; i < fields.Length; i++) + { + FieldInfo? field = currentObject?.GetType().GetField(fields[i]); + if (field == null) { break; } + + if (i == fields.Length - 1) + { + try + { + field.SetValue(currentObject, value); + } + catch { } + } + else + { + currentObject = field.GetValue(currentObject); + } + } + } + } + catch (Exception e) + { + Plugin.Logger.Error($"Error deserializing {type.Name}: " + e.Message); + } + + return config; + } + + + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + if (value == null) { return; } + + JObject jsonObject = new JObject(); + Type type = value.GetType(); + jsonObject.Add("$type", type.FullName + ", HSUI"); + + FieldInfo[] fields = type.GetFields(); + + foreach (FieldInfo field in fields) + { + if (field.GetCustomAttribute() != null) { continue; } + + object? fieldValue = field.GetValue(value); + if (fieldValue != null) + { + jsonObject.Add(field.Name, JToken.FromObject(fieldValue, serializer)); + } + } + + jsonObject.WriteTo(writer); + } + } + + #region contract resolver + public class PluginConfigObjectsContractResolver : DefaultContractResolver + { + private static Dictionary ConvertersMap = new Dictionary() + { + [typeof(UnitFrameConfig)] = typeof(ColorByHealthFieldsConverter), + [typeof(PlayerUnitFrameConfig)] = typeof(ColorByHealthFieldsConverter), + [typeof(TargetUnitFrameConfig)] = typeof(ColorByHealthFieldsConverter), + [typeof(TargetOfTargetUnitFrameConfig)] = typeof(ColorByHealthFieldsConverter), + [typeof(FocusTargetUnitFrameConfig)] = typeof(ColorByHealthFieldsConverter), + + [typeof(PartyFramesColorsConfig)] = typeof(ColorByHealthFieldsConverter), + [typeof(PartyFramesRoleIconConfig)] = typeof(PartyFramesIconsConverter), + [typeof(PartyFramesLeaderIconConfig)] = typeof(PartyFramesIconsConverter), + [typeof(PartyFramesRaiseTrackerConfig)] = typeof(PartyFramesTrackerConfigConverter), + [typeof(PartyFramesInvulnTrackerConfig)] = typeof(PartyFramesTrackerConfigConverter), + [typeof(PartyFramesManaBarConfig)] = typeof(PartyFramesManaBarConfigConverter), + + [typeof(StatusEffectsBlacklistConfig)] = typeof(StatusEffectsBlacklistConfigConverter), + + [typeof(HUDOptionsConfig)] = typeof(HUDOptionsConfigConverter), + + [typeof(CastbarConfig)] = typeof(CastbarConfigConverter), + [typeof(UnitFrameCastbarConfig)] = typeof(CastbarConfigConverter), + [typeof(PlayerCastbarConfig)] = typeof(CastbarConfigConverter), + [typeof(TargetCastbarConfig)] = typeof(CastbarConfigConverter), + [typeof(TargetOfTargetCastbarConfig)] = typeof(CastbarConfigConverter), + [typeof(FocusTargetCastbarConfig)] = typeof(CastbarConfigConverter), + [typeof(PartyFramesCastbarConfig)] = typeof(CastbarConfigConverter), + [typeof(EnemyListCastbarConfig)] = typeof(CastbarConfigConverter), + }; + + protected override JsonObjectContract CreateObjectContract(Type objectType) + { + JsonObjectContract contract = base.CreateObjectContract(objectType); + + if (ConvertersMap.TryGetValue(objectType, out Type? converterType) && converterType != null) + { + contract.Converter = (JsonConverter?)Activator.CreateInstance(converterType); + } + + return contract; + } + } + #endregion + + #region field converters + public abstract class PluginConfigObjectFieldConverter + { + public readonly string NewFieldPath; + public PluginConfigObjectFieldConverter(string newFieldPath) + { + NewFieldPath = newFieldPath; + } + + public abstract (string, object) Convert(JToken token); + } + + public class NewTypeFieldConverter : PluginConfigObjectFieldConverter + where TOld : struct + where TNew : struct + { + private TNew DefaultValue; + private Func Func; + + public NewTypeFieldConverter(string newFieldPath, TNew defaultValue, Func func) : base(newFieldPath) + { + DefaultValue = defaultValue; + Func = func; + } + + public override (string, object) Convert(JToken token) + { + TNew result = DefaultValue; + + TOld? oldValue = token.ToObject(); + if (oldValue.HasValue) + { + result = Func(oldValue.Value); + } + + return (NewFieldPath, result); + } + } + + public class SameTypeFieldConverter : NewTypeFieldConverter where T : struct + { + public SameTypeFieldConverter(string newFieldPath, T defaultValue) + : base(newFieldPath, defaultValue, (oldValue) => { return oldValue; }) + { + } + } + + public class NewClassFieldConverter : PluginConfigObjectFieldConverter + where TOld : class + where TNew : class + { + private TNew DefaultValue; + private Func Func; + + public NewClassFieldConverter(string newFieldPath, TNew defaultValue, Func func) + : base(newFieldPath) + { + DefaultValue = defaultValue; + Func = func; + } + + public override (string, object) Convert(JToken token) + { + TNew result = DefaultValue; + + TOld? oldValue = token.ToObject(); + if (oldValue != null) + { + result = Func(oldValue); + } + + return (NewFieldPath, result); + } + } + + public class SameClassFieldConverter : NewClassFieldConverter where T : class + { + public SameClassFieldConverter(string newFieldPath, T defaultValue) + : base(newFieldPath, defaultValue, (oldValue) => { return oldValue; }) + { + } + } + + public class TypeToClassFieldConverter : PluginConfigObjectFieldConverter + where TOld : struct + where TNew : class + { + private TNew DefaultValue; + private Func Func; + + public TypeToClassFieldConverter(string newFieldPath, TNew defaultValue, Func func) : base(newFieldPath) + { + DefaultValue = defaultValue; + Func = func; + } + + public override (string, object) Convert(JToken token) + { + TNew result = DefaultValue; + + TOld? oldValue = token.ToObject(); + if (oldValue.HasValue) + { + result = Func(oldValue.Value); + } + + return (NewFieldPath, result); + } + } + #endregion +} diff --git a/Config/Profiles/ImportExportHelper.cs b/Config/Profiles/ImportExportHelper.cs new file mode 100644 index 0000000..508dc43 --- /dev/null +++ b/Config/Profiles/ImportExportHelper.cs @@ -0,0 +1,48 @@ +using Newtonsoft.Json; +using System; +using System.IO; +using System.IO.Compression; +using System.Text; + +namespace HSUI.Config.Profiles +{ + public static class ImportExportHelper + { + public static string CompressAndBase64Encode(string jsonString) + { + using MemoryStream output = new(); + + using (DeflateStream gzip = new(output, CompressionLevel.Optimal)) + { + using StreamWriter writer = new(gzip, Encoding.UTF8); + writer.Write(jsonString); + } + + return Convert.ToBase64String(output.ToArray()); + } + + public static string Base64DecodeAndDecompress(string base64String) + { + var base64EncodedBytes = Convert.FromBase64String(base64String); + + using MemoryStream inputStream = new(base64EncodedBytes); + using DeflateStream gzip = new(inputStream, CompressionMode.Decompress); + using StreamReader reader = new(gzip, Encoding.UTF8); + var decodedString = reader.ReadToEnd(); + + return decodedString; + } + + public static string GenerateExportString(object obj) + { + JsonSerializerSettings settings = new JsonSerializerSettings + { + TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple, + TypeNameHandling = TypeNameHandling.Objects + }; + + var jsonString = JsonConvert.SerializeObject(obj, Formatting.Indented, settings); + return CompressAndBase64Encode(jsonString); + } + } +} diff --git a/Config/Profiles/Profile.cs b/Config/Profiles/Profile.cs new file mode 100644 index 0000000..399b116 --- /dev/null +++ b/Config/Profiles/Profile.cs @@ -0,0 +1,113 @@ +using HSUI.Helpers; +using System; +using System.Collections.Generic; + +namespace HSUI.Config.Profiles +{ + public class Profile + { + public string Name; + + public bool AutoSwitchEnabled = false; + public AutoSwitchData AutoSwitchData = new AutoSwitchData(); + public int HudLayout; + public bool AttachHudEnabled = false; + + public Profile(string name, bool autoSwitchEnabled = false, AutoSwitchData? autoSwitchData = null, bool attachHudEnabled = false, int hudLayout = 0) + { + Name = name; + + AutoSwitchEnabled = autoSwitchEnabled; + AutoSwitchData = autoSwitchData ?? AutoSwitchData; + + AttachHudEnabled = attachHudEnabled; + HudLayout = hudLayout; + } + } + + public class AutoSwitchData + { + public Dictionary> Map; + + public AutoSwitchData() + { + Map = new Dictionary>(); + + JobRoles[] roles = (JobRoles[])Enum.GetValues(typeof(JobRoles)); + + foreach (JobRoles role in roles) + { + int count = JobsHelper.JobsByRole[role].Count; + List list = new List(count); + + for (int i = 0; i < count; i++) + { + list.Add(false); + } + + Map.Add(role, list); + } + } + + public bool GetRoleEnabled(JobRoles role) + { + foreach (bool value in Map[role]) + { + if (!value) + { + return false; + } + } + + return true; + } + + public void SetRoleEnabled(JobRoles role, bool value) + { + for (int i = 0; i < Map[role].Count; i++) + { + Map[role][i] = value; + } + } + + public bool IsEnabled(JobRoles role, int index) + { + if (Map.TryGetValue(role, out List? list) && list != null) + { + if (index >= list.Count) + { + return false; + } + + return list[index]; + } + + return false; + } + + public bool ValidateRolesData() + { + bool changed = false; + + JobRoles[] roles = (JobRoles[])Enum.GetValues(typeof(JobRoles)); + + foreach (JobRoles role in roles) + { + int count = JobsHelper.JobsByRole[role].Count; + List list = Map[role]; + + if (list.Count < count) + { + for (int i = 0; i < count - list.Count; i++) + { + list.Add(false); + } + + changed = true; + } + } + + return changed; + } + } +} diff --git a/Config/Profiles/ProfilesManager.cs b/Config/Profiles/ProfilesManager.cs new file mode 100644 index 0000000..587d0f3 --- /dev/null +++ b/Config/Profiles/ProfilesManager.cs @@ -0,0 +1,1024 @@ +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Interface; +using Dalamud.Interface.ImGuiFileDialog; +using Dalamud.Logging; +using HSUI.Config.Attributes; +using HSUI.Config.Tree; +using HSUI.Helpers; +using HSUI.Interface; +using Dalamud.Bindings.ImGui; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Numerics; +using FFXIVClientStructs.FFXIV.Client.UI.Misc; + +namespace HSUI.Config.Profiles +{ + public class ProfilesManager + { + #region Singleton + public readonly SectionNode ProfilesNode; + + private ProfilesManager() + { + // fake nodes + ProfilesNode = new SectionNode(); + ProfilesNode.Name = "Profiles"; + + NestedSubSectionNode subSectionNode = new NestedSubSectionNode(); + subSectionNode.Name = "General"; + subSectionNode.Depth = 0; + + ProfilesConfigPageNode configPageNode = new ProfilesConfigPageNode(); + + subSectionNode.Add(configPageNode); + ProfilesNode.Add(subSectionNode); + + ConfigurationManager.Instance.AddExtraSectionNode(ProfilesNode); + + // default profile + if (!Profiles.ContainsKey(DefaultProfileName)) + { + Profile defaultProfile = new Profile(DefaultProfileName); + Profiles.Add(DefaultProfileName, defaultProfile); + } + } + + private bool ResetProfileToDefault(string profileName, bool forced = false) + { + string endPath = Path.Combine(ProfilesPath, profileName + ".HSUI"); + + if (forced) + { + try + { + File.Delete(endPath); + } catch { } + } + + if (forced || !File.Exists(endPath)) + { + try + { + Directory.CreateDirectory(ProfilesPath); + File.Copy(MediaDefaultProfilePath, endPath); + + return true; + } + catch (Exception e) + { + Plugin.Logger.Error("Error copying default profile!: " + e.Message); + } + } + + return false; + } + + public static void Initialize() + { + bool attemptRepair = false; + + try + { + string jsonString = File.ReadAllText(JsonPath); + ProfilesManager? instance = JsonConvert.DeserializeObject(jsonString); + if (instance != null) + { + Instance = instance; + + bool needsSave = false; + foreach (Profile profile in Instance.Profiles.Values) + { + needsSave |= profile.AutoSwitchData.ValidateRolesData(); + } + + if (needsSave) + { + Instance.Save(); + } + } + } + catch + { + Instance = new ProfilesManager(); + attemptRepair = true; + } + + if (Instance == null) + { + Plugin.Logger.Error("Error initializing HSUI's profiles!!!"); + return; + } + + // attempt to reconstruct profile from files if the Profiles directory is missing + if (attemptRepair && + !ConfigurationManager.Instance.IsFreshInstall() && + !Directory.Exists(ProfilesPath)) + { + Instance.CurrentProfileName = "Restored Profile"; + + Profile defaultProfile = new Profile(Instance.CurrentProfileName); + Instance.Profiles.Add(Instance.CurrentProfileName, defaultProfile); + } + + // always make sure the default profile file is present + if (!File.Exists(DefaultProfilePath)) + { + if (Instance.ResetProfileToDefault(DefaultProfileName)) + { + if (Instance.CurrentProfileName == DefaultProfileName) + { + Instance.ReloadCurrentProfile(); + } + } + } + + Instance.UpdateSelectedIndex(); + Instance.InitializeDefaultImportData(); + } + + private void InitializeDefaultImportData() + { + string importString = File.ReadAllText(MediaDefaultProfilePath); + + string[] importStrings = importString.Trim().Split(new string[] { "|" }, StringSplitOptions.RemoveEmptyEntries); + if (importStrings.Length == 0) + { + return; + } + + foreach (var str in importStrings) + { + try + { + ImportData importData = new ImportData(str); + _defaultImportData.Add(importData); + } + catch { } + } + } + + public static ProfilesManager Instance { get; private set; } = null!; + + ~ProfilesManager() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + + Instance = null!; + } + #endregion + + private string _currentProfileName = "Default"; + public string CurrentProfileName + { + get => _currentProfileName; + set + { + if (_currentProfileName == value) + { + return; + } + + _currentProfileName = value; + + if (_currentProfileName == null || _currentProfileName.Length == 0) + { + _currentProfileName = DefaultProfileName; + } + + UpdateSelectedIndex(); + } + } + + [JsonIgnore] private static string ProfilesPath => Path.Combine(ConfigurationManager.Instance.ConfigDirectory, "Profiles"); + [JsonIgnore] private static string JsonPath => Path.Combine(ProfilesPath, "Profiles.json"); + [JsonIgnore] private static string MediaDefaultProfilePath => Path.Combine(Plugin.AssemblyLocation, "Media", "Profiles", DefaultProfileName + ".HSUI"); + + [JsonIgnore] private static string DefaultProfileName = "Default"; + [JsonIgnore] private static string DefaultProfilePath = Path.Combine(ProfilesPath, DefaultProfileName + ".HSUI"); + + [JsonIgnore] private List _defaultImportData = new List(); + + [JsonIgnore] private string _newProfileName = ""; + [JsonIgnore] private int _copyFromIndex = 0; + [JsonIgnore] private int _selectedProfileIndex = 0; + [JsonIgnore] private string? _errorMessage = null; + [JsonIgnore] private string? _deletingProfileName = null; + [JsonIgnore] private string? _resetingProfileName = null; + [JsonIgnore] private string? _renamingProfileName = null; + + [JsonIgnore] private FileDialogManager _fileDialogManager = new FileDialogManager(); + + public SortedList Profiles = new SortedList(); + + public ImportData? DefaultImportData(Type type) + { + return _defaultImportData.FirstOrDefault(o => o.ConfigType == type); + } + + public Profile CurrentProfile() + { + if (_currentProfileName == null || _currentProfileName.Length == 0) + { + _currentProfileName = DefaultProfileName; + } + + return Profiles[_currentProfileName]; + } + + public void SaveCurrentProfile() + { + if (ConfigurationManager.Instance == null) + { + return; + } + + try + { + Save(); + SaveCurrentProfile(ConfigurationManager.Instance.ExportCurrentConfigs()); + } + catch (Exception e) + { + Plugin.Logger.Error("Error saving profile: " + e.Message); + } + } + + public void SaveCurrentProfile(string? exportString) + { + if (exportString == null) + { + return; + } + + try + { + Directory.CreateDirectory(ProfilesPath); + + File.WriteAllText(CurrentProfilePath(), exportString); + } + catch (Exception e) + { + Plugin.Logger.Error("Error saving profile: " + e.Message); + } + } + + public bool LoadCurrentProfile(string oldProfile) + { + try + { + string importString = File.ReadAllText(CurrentProfilePath()); + return ConfigurationManager.Instance.ImportProfile(oldProfile, _currentProfileName, importString); + } + catch (Exception e) + { + Plugin.Logger.Error("Error loading profile: " + e.Message); + } + + return false; + } + + private bool ReloadCurrentProfile() + { + try + { + string importString = File.ReadAllText(CurrentProfilePath()); + if (ConfigurationManager.Instance.ImportProfile(_currentProfileName, _currentProfileName, importString, true)) + { + ConfigurationManager.Instance.SaveConfigurations(true); + return true; + } + } + catch (Exception e) + { + Plugin.Logger.Error("Error re-loading profile: " + e.Message); + } + + return false; + } + + public void UpdateCurrentProfile() + { + IPlayerCharacter? player = Plugin.ObjectTable.LocalPlayer; + if (player == null) + { + return; + } + + uint jobId = player.ClassJob.RowId; + Profile currentProfile = CurrentProfile(); + JobRoles role = JobsHelper.RoleForJob(jobId); + int index = JobsHelper.JobsByRole[role].IndexOf(jobId); + + if (index < 0) + { + return; + } + + // current profile is enabled for this job, do nothing + if (currentProfile.AutoSwitchEnabled && currentProfile.AutoSwitchData.IsEnabled(role, index)) + { + return; + } + + // find a profile that is enabled for this job + foreach (Profile profile in Profiles.Values) + { + if (!profile.AutoSwitchEnabled || profile == currentProfile) + { + continue; + } + + // found a valid profile, switch to it + if (profile.AutoSwitchData.IsEnabled(role, index)) + { + SwitchToProfile(profile.Name); + return; + } + } + } + + public void CheckUpdateSwitchCurrentProfile(string specifiedProfile) + { + // found a valid profile, switch to it + if (Profiles.ContainsKey(specifiedProfile)) + { + SwitchToProfile(specifiedProfile); + } + } + + private unsafe string? SwitchToProfile(string profile, bool save = true) + { + // save if needed before switching + if (save) + { + ConfigurationManager.Instance.SaveConfigurations(); + } + + string oldProfile = _currentProfileName; + _currentProfileName = profile; + Profile currentProfile = CurrentProfile(); + + if (currentProfile.AttachHudEnabled && currentProfile.HudLayout != 0) + { + AddonConfig.Instance()->ChangeHudLayout((uint)currentProfile.HudLayout - 1); + } + + if (!LoadCurrentProfile(oldProfile)) + { + _currentProfileName = oldProfile; + return "Couldn't load profile \"" + profile + "\"!"; + } + + UpdateSelectedIndex(); + + try + { + Save(); + FontsManager.Instance.BuildFonts(); + } + catch (Exception e) + { + Plugin.Logger.Error("Error saving profile: " + e.Message); + return "Couldn't load profile \"" + profile + "\"!"; + } + + return null; + } + + private void UpdateSelectedIndex() + { + _selectedProfileIndex = Math.Max(0, Profiles.IndexOfKey(_currentProfileName)); + } + + private string CurrentProfilePath() + { + return Path.Combine(ProfilesPath, _currentProfileName + ".HSUI"); + } + + private string? CloneProfile(string profileName, string newProfileName) + { + string srcPath = Path.Combine(ProfilesPath, profileName + ".HSUI"); + string dstPath = Path.Combine(ProfilesPath, newProfileName + ".HSUI"); + + return CloneProfile(profileName, srcPath, newProfileName, dstPath); + } + + private string? CloneProfile(string profileName, string srcPath, string newProfileName, string dstPath) + { + if (newProfileName.Length == 0) + { + return null; + } + + if (Profiles.Keys.Contains(newProfileName)) + { + return "A profile with the name \"" + newProfileName + "\" already exists!"; + } + + try + { + if (!File.Exists(srcPath)) + { + return "Couldn't find profile \"" + profileName + "\"!"; + } + + if (File.Exists(dstPath)) + { + return "A profile with the name \"" + newProfileName + "\" already exists!"; + } + + File.Copy(srcPath, dstPath); + Profile newProfile = new Profile(newProfileName); + Profiles.Add(newProfileName, newProfile); + + Save(); + } + catch (Exception e) + { + Plugin.Logger.Error("Error cloning profile: " + e.Message); + return "Error trying to clone profile \"" + profileName + "\"!"; + } + + return null; + } + + private string? RenameCurrentProfile(string newProfileName) + { + if (_currentProfileName == newProfileName || newProfileName.Length == 0) + { + return null; + } + + if (Profiles.ContainsKey(newProfileName)) + { + return "A profile with the name \"" + newProfileName + "\" already exists!"; + } + + string srcPath = Path.Combine(ProfilesPath, _currentProfileName + ".HSUI"); + string dstPath = Path.Combine(ProfilesPath, newProfileName + ".HSUI"); + + try + { + + if (File.Exists(dstPath)) + { + return "A profile with the name \"" + newProfileName + "\" already exists!"; + } + + File.Move(srcPath, dstPath); + + Profile profile = Profiles[_currentProfileName]; + profile.Name = newProfileName; + + Profiles.Remove(_currentProfileName); + Profiles.Add(newProfileName, profile); + + _currentProfileName = newProfileName; + + Save(); + } + catch (Exception e) + { + Plugin.Logger.Error("Error renaming profile: " + e.Message); + return "Error trying to rename profile \"" + _currentProfileName + "\"!"; + } + + return null; + } + + private string? Import(string newProfileName, string importString) + { + if (newProfileName.Length == 0) + { + return null; + } + + if (Profiles.Keys.Contains(newProfileName)) + { + return "A profile with the name \"" + newProfileName + "\" already exists!"; + } + + string dstPath = Path.Combine(ProfilesPath, newProfileName + ".HSUI"); + + try + { + if (File.Exists(dstPath)) + { + return "A profile with the name \"" + newProfileName + "\" already exists!"; + } + + File.WriteAllText(dstPath, importString); + + Profile newProfile = new Profile(newProfileName); + Profiles.Add(newProfileName, newProfile); + + string? errorMessage = SwitchToProfile(newProfileName, false); + + if (errorMessage != null) + { + Profiles.Remove(newProfileName); + File.Delete(dstPath); + Save(); + + return errorMessage; + } + } + catch (Exception e) + { + Plugin.Logger.Error("Error importing profile: " + e.Message); + return "Error trying to import profile \"" + newProfileName + "\"!"; + } + + return null; + } + + private string? ImportFromClipboard(string newProfileName) + { + string importString = ImGui.GetClipboardText(); + if (importString.Length == 0) + { + return "Invalid import string!"; + } + + return Import(newProfileName, importString); + } + + private void ImportFromFile(string newProfileName) + { + if (newProfileName.Length == 0) + { + return; + } + + Action callback = (finished, path) => + { + try + { + if (finished && path.Length > 0) + { + string importString = File.ReadAllText(path); + _errorMessage = Import(newProfileName, importString); + + if (_errorMessage == null) + { + _newProfileName = ""; + } + } + } + catch (Exception e) + { + Plugin.Logger.Error("Error reading import file: " + e.Message); + _errorMessage = "Error reading the file!"; + } + }; + + _fileDialogManager.OpenFileDialog("Select a HSUI Profile to import", "HSUI Profile{.HSUI}", callback); + } + + private void ExportToFile(string newProfileName) + { + if (newProfileName.Length == 0) + { + return; + } + + Action callback = (finished, path) => + { + try + { + string src = CurrentProfilePath(); + if (finished && path.Length > 0 && src != path) + { + File.Copy(src, path, true); + } + } + catch (Exception e) + { + Plugin.Logger.Error("Error copying file: " + e.Message); + _errorMessage = "Error exporting the file!"; + } + }; + + _fileDialogManager.SaveFileDialog("Save Profile", "HSUI Profile{.HSUI}", newProfileName + ".HSUI", ".HSUI", callback); + } + + private string? DeleteProfile(string profileName) + { + if (!Profiles.ContainsKey(profileName)) + { + return "Couldn't find profile \"" + profileName + "\"!"; + } + + string path = Path.Combine(ProfilesPath, profileName + ".HSUI"); + + try + { + if (!File.Exists(path)) + { + return "Couldn't find profile \"" + profileName + "\"!"; + } + + File.Delete(path); + Profiles.Remove(profileName); + + Save(); + + ConfigurationManager.Instance.OnProfileDeleted(profileName); + + if (_currentProfileName == profileName) + { + return SwitchToProfile(DefaultProfileName, false); + } + } + catch (Exception e) + { + Plugin.Logger.Error("Error deleting profile: " + e.Message); + return "Error trying to delete profile \"" + profileName + "\"!"; + } + + return null; + } + + private void Save() + { + string jsonString = JsonConvert.SerializeObject( + this, + Formatting.Indented, + new JsonSerializerSettings + { + TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple, + TypeNameHandling = TypeNameHandling.Objects + } + ); + + Directory.CreateDirectory(ProfilesPath); + File.WriteAllText(JsonPath, jsonString); + } + + public bool Draw(ref bool changed) + { + string[] profiles = Profiles.Keys.ToArray(); + + if (ImGui.BeginChild("Profiles", new Vector2(800, 600), false)) + { + if (Profiles.Count == 0) + { + ImGuiHelper.Tab(); + ImGui.Text("Profiles not found in \"%appdata%/Roaming/XIVLauncher/pluginConfigs/HSUI/Profiles/\""); + return false; + } + + ImGui.PushItemWidth(408); + ImGuiHelper.NewLineAndTab(); + if (ImGui.Combo("Active Profile", ref _selectedProfileIndex, profiles, 10)) + { + string newProfileName = profiles[_selectedProfileIndex]; + + if (_currentProfileName != newProfileName) + { + _errorMessage = SwitchToProfile(newProfileName); + } + } + + // reset + ImGui.SameLine(); + ImGui.PushFont(UiBuilder.IconFont); + if (ImGui.Button("\uf2f9", new Vector2(0, 0))) + { + _resetingProfileName = _currentProfileName; + } + ImGui.PopFont(); + ImGuiHelper.SetTooltip("Reset"); + + if (_currentProfileName != DefaultProfileName) + { + // rename + ImGui.SameLine(); + ImGui.PushFont(UiBuilder.IconFont); + if (ImGui.Button(FontAwesomeIcon.Pen.ToIconString())) + { + _renamingProfileName = _currentProfileName; + } + ImGui.PopFont(); + ImGuiHelper.SetTooltip("Rename"); + + // delete + ImGui.SameLine(); + ImGui.PushFont(UiBuilder.IconFont); + if (_currentProfileName != DefaultProfileName && ImGui.Button(FontAwesomeIcon.Trash.ToIconString())) + { + _deletingProfileName = _currentProfileName; + } + ImGui.PopFont(); + ImGuiHelper.SetTooltip("Delete"); + } + + // export to string + ImGuiHelper.Tab(); + ImGui.SameLine(); + if (ImGui.Button("Export to Clipboard", new Vector2(200, 0))) + { + string? exportString = ConfigurationManager.Instance.ExportCurrentConfigs(); + if (exportString != null) + { + ImGui.SetClipboardText(exportString); + ImGui.OpenPopup("export_succes_popup"); + } + } + + // export success popup + if (ImGui.BeginPopup("export_succes_popup")) + { + ImGui.Text("Profile export string copied to clipboard!"); + ImGui.EndPopup(); + } + + ImGui.SameLine(); + if (ImGui.Button("Export to File", new Vector2(200, 0))) + { + ExportToFile(_currentProfileName); + } + + ImGuiHelper.NewLineAndTab(); + DrawAttachHudLayout(ref changed); + + ImGuiHelper.NewLineAndTab(); + DrawAutoSwitchSettings(ref changed); + + ImGuiHelper.DrawSeparator(1, 1); + ImGuiHelper.Tab(); + ImGui.Text("Create a new profile:"); + + ImGuiHelper.Tab(); + ImGui.PushItemWidth(408); + ImGui.InputText("Profile Name", ref _newProfileName, 200); + + ImGuiHelper.Tab(); + ImGui.PushItemWidth(200); + ImGui.Combo("", ref _copyFromIndex, profiles, 10); + + ImGui.SameLine(); + if (ImGui.Button("Copy", new Vector2(200, 0))) + { + _newProfileName = _newProfileName.Trim(); + if (_newProfileName.Length == 0) + { + ImGui.OpenPopup("import_error_popup"); + } + else + { + _errorMessage = CloneProfile(profiles[_copyFromIndex], _newProfileName); + + if (_errorMessage == null) + { + _errorMessage = SwitchToProfile(_newProfileName); + _newProfileName = ""; + } + } + } + + ImGuiHelper.NewLineAndTab(); + if (ImGui.Button("Import From Clipboard", new Vector2(200, 0))) + { + _newProfileName = _newProfileName.Trim(); + if (_newProfileName.Length == 0) + { + ImGui.OpenPopup("import_error_popup"); + } + else + { + _errorMessage = ImportFromClipboard(_newProfileName); + + if (_errorMessage == null) + { + _newProfileName = ""; + } + } + } + + ImGui.SameLine(); + if (ImGui.Button("Import From File", new Vector2(200, 0))) + { + _newProfileName = _newProfileName.Trim(); + if (_newProfileName.Length == 0) + { + ImGui.OpenPopup("import_error_popup"); + } + else + { + ImportFromFile(_newProfileName); + } + } + + // no name popup + if (ImGui.BeginPopup("import_error_popup")) + { + ImGui.Text("Please type a name for the new profile!"); + ImGui.EndPopup(); + } + } + + ImGui.EndChild(); + + // error message + if (_errorMessage != null) + { + if (ImGuiHelper.DrawErrorModal(_errorMessage)) + { + _errorMessage = null; + } + } + + // delete confirmation + if (_deletingProfileName != null) + { + string[] lines = new string[] { "Are you sure you want to delete the profile:", " - " + _deletingProfileName }; + var (didConfirm, didClose) = ImGuiHelper.DrawConfirmationModal("Delete?", lines); + + if (didConfirm) + { + _errorMessage = DeleteProfile(_deletingProfileName); + changed = true; + } + + if (didConfirm || didClose) + { + _deletingProfileName = null; + } + } + + // reset confirmation + if (_resetingProfileName != null) + { + string[] lines = new string[] { "Are you sure you want to reset the profile:", " - " + _resetingProfileName }; + var (didConfirm, didClose) = ImGuiHelper.DrawConfirmationModal("Reset?", lines); + + if (didConfirm) + { + ResetProfileToDefault(_resetingProfileName, true); + ReloadCurrentProfile(); + + changed = true; + } + + if (didConfirm || didClose) + { + _resetingProfileName = null; + } + } + + // rename modal + if (_renamingProfileName != null) + { + var (didConfirm, didClose) = ImGuiHelper.DrawInputModal("Rename", "Type a new name for the profile:", ref _renamingProfileName); + + if (didConfirm) + { + _errorMessage = RenameCurrentProfile(_renamingProfileName); + + changed = true; + } + + if (didConfirm || didClose) + { + _renamingProfileName = null; + } + } + + _fileDialogManager.Draw(); + + return false; + } + + private void DrawAutoSwitchSettings(ref bool changed) + { + Profile profile = CurrentProfile(); + + changed |= ImGui.Checkbox("Auto-Switch For Specific Jobs", ref profile.AutoSwitchEnabled); + + if (!profile.AutoSwitchEnabled) + { + return; + } + + AutoSwitchData data = profile.AutoSwitchData; + Vector2 cursorPos = ImGui.GetCursorPos() + new Vector2(14, 14); + Vector2 originalPos = cursorPos; + float maxY = 0; + + JobRoles[] roles = (JobRoles[])Enum.GetValues(typeof(JobRoles)); + + foreach (JobRoles role in roles) + { + if (role == JobRoles.Unknown) { continue; } + if (!data.Map.ContainsKey(role)) { continue; } + + bool roleValue = data.GetRoleEnabled(role); + string roleName = JobsHelper.RoleNames[role]; + + ImGui.SetCursorPos(cursorPos); + if (ImGui.Checkbox(roleName, ref roleValue)) + { + data.SetRoleEnabled(role, roleValue); + changed = true; + } + + cursorPos.Y += 40; + int jobCount = data.Map[role].Count; + + for (int i = 0; i < jobCount; i++) + { + maxY = Math.Max(cursorPos.Y, maxY); + uint jobId = JobsHelper.JobsByRole[role][i]; + bool jobValue = data.Map[role][i]; + string jobName = JobsHelper.JobNames[jobId]; + + ImGui.SetCursorPos(cursorPos); + if (ImGui.Checkbox(jobName, ref jobValue)) + { + data.Map[role][i] = jobValue; + changed = true; + } + + cursorPos.Y += 30; + } + + cursorPos.X += 100; + cursorPos.Y = originalPos.Y; + } + + ImGui.SetCursorPos(new Vector2(originalPos.X, maxY + 30)); + } + + private void DrawAttachHudLayout(ref bool changed) + { + Profile profile = CurrentProfile(); + + changed |= ImGui.Checkbox("Attach HUD Layout to this profile", ref profile.AttachHudEnabled); + + if (!profile.AttachHudEnabled) + { + profile.HudLayout = 0; + return; + } + + int hudLayout = profile.HudLayout; + + ImGui.Text("\u2514"); + + for (int i = 1; i <= 4; i++) + { + ImGui.SameLine(); + bool hudLayoutEnabled = hudLayout == i; + if (ImGui.Checkbox("Hud Layout " + i, ref hudLayoutEnabled)) + { + profile.HudLayout = i; + changed = true; + } + } + + } + } + + // fake config object + [Disableable(false)] + [Exportable(false)] + [Shareable(false)] + [Resettable(false)] + public class ProfilesConfig : PluginConfigObject + { + public new static ProfilesConfig DefaultConfig() { return new ProfilesConfig(); } + } + + // fake config page node + public class ProfilesConfigPageNode : ConfigPageNode + { + public ProfilesConfigPageNode() + { + ConfigObject = new ProfilesConfig(); + } + + public override bool Draw(ref bool changed) + { + return ProfilesManager.Instance?.Draw(ref changed) ?? false; + } + } +} diff --git a/Config/Tree/BaseNode.cs b/Config/Tree/BaseNode.cs new file mode 100644 index 0000000..606cb4c --- /dev/null +++ b/Config/Tree/BaseNode.cs @@ -0,0 +1,372 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Textures.TextureWraps; +using Dalamud.Interface.Utility; +using HSUI.Config.Attributes; +using HSUI.Helpers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace HSUI.Config.Tree +{ + public delegate void ConfigObjectResetEventHandler(BaseNode sender); + + public class BaseNode : Node + { + public event ConfigObjectResetEventHandler? ConfigObjectResetEvent; + + private Dictionary _configPageNodesMap; + + public bool NeedsSave = false; + public string? SelectedOptionName = null; + + private List _extraNodes = new List(); + private List _nodes = new List(); + + public IReadOnlyCollection Sections => _nodes.AsReadOnly(); + + private float _scale => ImGuiHelpers.GlobalScale; + + public BaseNode() + { + _configPageNodesMap = new Dictionary(); + } + + public void AddExtraSectionNode(SectionNode node) + { + _extraNodes.Add(node); + + _nodes.Clear(); + CreateNodesIfNeeded(); + } + + public T? GetConfigObject() where T : PluginConfigObject + { + var pageNode = GetConfigPageNode(); + + return pageNode != null ? (T)pageNode.ConfigObject : null; + } + + public void RemoveConfigObject() where T : PluginConfigObject + { + if (_configPageNodesMap.ContainsKey(typeof(T))) + { + _configPageNodesMap.Remove(typeof(T)); + } + } + + public ConfigPageNode? GetConfigPageNode() where T : PluginConfigObject + { + if (_configPageNodesMap.TryGetValue(typeof(T), out var node)) + { + return node; + } + + var configPageNode = GetOrAddConfig(); + + if (configPageNode != null && configPageNode.ConfigObject != null) + { + _configPageNodesMap.Add(typeof(T), configPageNode); + + return configPageNode; + } + + return null; + } + + public void SetConfigPageNode(ConfigPageNode configPageNode) + { + if (configPageNode.ConfigObject == null) + { + return; + } + + _configPageNodesMap[configPageNode.ConfigObject.GetType()] = configPageNode; + } + + public bool SetConfigObject(PluginConfigObject configObject) + { + if (_configPageNodesMap.TryGetValue(configObject.GetType(), out ConfigPageNode? configPageNode)) + { + configPageNode.ConfigObject = configObject; + return true; + } + + return false; + } + + private bool PushStyles() + { + ImGui.PushStyleVar(ImGuiStyleVar.ScrollbarRounding, 1); //Scrollbar Radius + ImGui.PushStyleVar(ImGuiStyleVar.TabRounding, 1); //Tabs Radius Radius + ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 1); //Intractable Elements Radius + ImGui.PushStyleVar(ImGuiStyleVar.GrabRounding, 1); //Gradable Elements Radius + ImGui.PushStyleVar(ImGuiStyleVar.PopupRounding, 1); //Popup Radius + ImGui.PushStyleVar(ImGuiStyleVar.ScrollbarSize, 10); //Popup Radius + + if (ConfigurationManager.Instance.OverrideDalamudStyle) + { + ImGui.PushStyleColor(ImGuiCol.Button, new Vector4(46f / 255f, 45f / 255f, 46f / 255f, 1f)); + ImGui.PushStyleColor(ImGuiCol.ButtonActive, new Vector4(229f / 255f, 57f / 255f, 57f / 255f, .2f)); + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, new Vector4(229f / 255f, 57f / 255f, 57f / 255f, .2f)); + + ImGui.PushStyleColor(ImGuiCol.Separator, new Vector4(229f / 255f, 57f / 255f, 57f / 255f, .4f)); + + ImGui.PushStyleColor(ImGuiCol.ScrollbarBg, new Vector4(20f / 255f, 21f / 255f, 20f / 255f, .7f)); + ImGui.PushStyleColor(ImGuiCol.ScrollbarGrab, new Vector4(229f / 255f, 57f / 255f, 57f / 255f, .7f)); + ImGui.PushStyleColor(ImGuiCol.ScrollbarGrabActive, new Vector4(229f / 255f, 57f / 255f, 57f / 255f, .7f)); + ImGui.PushStyleColor(ImGuiCol.ScrollbarGrabHovered, new Vector4(229f / 255f, 57f / 255f, 57f / 255f, .7f)); + + ImGui.PushStyleColor(ImGuiCol.Tab, new Vector4(46f / 255f, 45f / 255f, 46f / 255f, 1f)); + ImGui.PushStyleColor(ImGuiCol.TabActive, new Vector4(229f / 255f, 57f / 255f, 57f / 255f, .7f)); + ImGui.PushStyleColor(ImGuiCol.TabHovered, new Vector4(229f / 255f, 57f / 255f, 57f / 255f, .2f)); + ImGui.PushStyleColor(ImGuiCol.TabUnfocused, new Vector4(229f / 255f, 57f / 255f, 57f / 255f, .2f)); + + ImGui.PushStyleColor(ImGuiCol.Border, new Vector4(229f / 255f, 57f / 255f, 57f / 255f, 1f)); + ImGui.PushStyleColor(ImGuiCol.CheckMark, new Vector4(229f / 255f, 57f / 255f, 57f / 255f, 1f)); + + ImGui.PushStyleColor(ImGuiCol.TableBorderStrong, new Vector4(229f / 255f, 57f / 255f, 57f / 255f, 1f)); + ImGui.PushStyleColor(ImGuiCol.TableBorderLight, new Vector4(229f / 255f, 57f / 255f, 57f / 255f, .4f)); + ImGui.PushStyleColor(ImGuiCol.TableHeaderBg, new Vector4(229f / 255f, 57f / 255f, 57f / 255f, .2f)); + + return true; + } + + return false; + } + + private void PopStyles(bool popColors) + { + if (popColors) + { + ImGui.PopStyleColor(17); + } + + ImGui.PopStyleVar(6); + } + + public void CreateNodesIfNeeded() + { + if (_nodes.Count > 0) { return; } + + _nodes.AddRange(_children); + _nodes.AddRange(_extraNodes); + } + + public void RefreshSelectedNode() + { + if (_nodes.Count == 0) { return; } + + foreach (SectionNode node in _nodes.FindAll(x => x is SectionNode)) + { + node.Selected = node.Name == SelectedOptionName; + } + } + + public void Draw(float alpha) + { + CreateNodesIfNeeded(); + if (_nodes.Count == 0) { return; } + + bool changed = false; + bool didReset = false; + + bool popColors = PushStyles(); + + ImGui.BeginGroup(); // Middle section + { + ImGui.BeginGroup(); // Left + { + // banner + IDalamudTextureWrap? delvUiBanner = Plugin.BannerTexture?.GetWrapOrDefault(); + if (delvUiBanner != null) + { + ImGui.SetCursorPos(new Vector2(15 + 150 * _scale / 2f - delvUiBanner.Width / 2f, 5)); + ImGui.Image(delvUiBanner.Handle, new Vector2(delvUiBanner.Width, delvUiBanner.Height)); + } + + // version + ImGui.SetCursorPos(new Vector2(60 * _scale, 35)); + ImGui.Text($"v{Plugin.Version}"); + + + // section list + if (ImGui.BeginChild("left pane", new Vector2(150 * _scale, -10), true, ImGuiWindowFlags.NoScrollbar)) + { + + // if no section is selected, select the first + if (_nodes.Any() && _nodes.Find(o => o is SectionNode sectionNode && sectionNode.Selected) == null) + { + SectionNode? selectedSection = (SectionNode?)_nodes.Find(o => o is SectionNode sectionNode && sectionNode.Name == SelectedOptionName); + if (selectedSection != null) + { + selectedSection.Selected = true; + SelectedOptionName = selectedSection.Name; + } + else if (_nodes.Count > 0) + { + SectionNode node = (SectionNode)_nodes[0]; + node.Selected = true; + SelectedOptionName = node.Name; + } + } + + foreach (SectionNode selectionNode in _nodes) + { + if (ImGui.Selectable(selectionNode.Name, selectionNode.Selected)) + { + selectionNode.Selected = true; + SelectedOptionName = selectionNode.Name; + + foreach (SectionNode otherNode in _nodes.FindAll(x => x != selectionNode)) + { + otherNode.Selected = false; + } + } + + DrawExportResetContextMenu(selectionNode, selectionNode.Name); + } + + // changelog button + float y = ImGui.GetWindowHeight() - (30 * _scale); + ImGui.PushFont(UiBuilder.IconFont); + ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(255f / 255f, 255f / 255f, 255f / 255f, alpha)); + ImGui.SetCursorPos(new Vector2(19 * _scale, y)); + if (ImGui.Button(FontAwesomeIcon.List.ToIconString(), new Vector2(24 * _scale, 24 * _scale))) + { + ConfigurationManager.Instance.OpenChangelogWindow(); + } + ImGui.PopStyleColor(); + ImGui.PopFont(); + ImGuiHelper.SetTooltip("Changelog"); + + // discord button + ImGui.PushFont(UiBuilder.IconFont); + ImGui.PushStyleColor(ImGuiCol.Button, new Vector4(114f / 255f, 137f / 255f, 218f / 255f, alpha)); + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, new Vector4(114f / 255f, 137f / 255f, 218f / 255f, alpha * .85f)); + ImGui.SetCursorPos(new Vector2(48 * _scale, y)); + if (ImGui.Button(FontAwesomeIcon.Link.ToIconString(), new Vector2(24 * _scale, 24 * _scale))) + { + Utils.OpenUrl("https://discord.gg/xzde5qQayh"); + } + ImGui.PopStyleColor(2); + ImGui.PopFont(); + ImGuiHelper.SetTooltip("HSUI Discord"); + + // discord button + ImGui.PushFont(UiBuilder.IconFont); + ImGui.PushStyleColor(ImGuiCol.Button, new Vector4(255f / 255f, 94f / 255f, 91f / 255f, alpha)); + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, new Vector4(255f / 255f, 94f / 255f, 91f / 255f, alpha * .85f)); + ImGui.SetCursorPos(new Vector2(77 * _scale, y)); + if (ImGui.Button(FontAwesomeIcon.MugHot.ToIconString(), new Vector2(24 * _scale, 24 * _scale))) + { + Utils.OpenUrl("https://ko-fi.com/Tischel"); + } + ImGui.PopStyleColor(2); + ImGui.PopFont(); + ImGuiHelper.SetTooltip("Tip the developer at ko-fi.com"); + } + + ImGui.EndChild(); + } + + ImGui.EndGroup(); // Left + + + didReset |= DrawResetModal(); + + ImGui.SameLine(); + + ImGui.BeginGroup(); // Right + { + foreach (SectionNode selectionNode in _nodes) + { + didReset |= selectionNode.Draw(ref changed, alpha); + } + } + + ImGui.EndGroup(); // Right + } + + ImGui.EndGroup(); // Middle section + + // close button + ImGui.PushFont(UiBuilder.IconFont); + ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(229f / 255f, 57f / 255f, 57f / 255f, alpha)); + ImGui.SetCursorPos(new Vector2(ImGui.GetWindowWidth() - 28 * _scale, 5 * _scale)); + if (ImGui.Button(FontAwesomeIcon.Times.ToIconString(), new Vector2(22 * _scale, 22 * _scale))) + { + ConfigurationManager.Instance.CloseConfigWindow(); + } + ImGui.PopStyleColor(); + ImGui.PopFont(); + ImGuiHelper.SetTooltip("Close"); + + // unlock button + ImGui.PushFont(UiBuilder.IconFont); + ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(229f / 255f, 57f / 255f, 57f / 255f, alpha)); + ImGui.SetCursorPos(new Vector2(ImGui.GetWindowWidth() - 60 * _scale, 5 * _scale)); + string lockString = ConfigurationManager.Instance.LockHUD ? FontAwesomeIcon.Lock.ToIconString() : FontAwesomeIcon.LockOpen.ToIconString(); + if (ImGui.Button(lockString, new Vector2(22 * _scale, 22 * _scale))) + { + ConfigurationManager.Instance.LockHUD = !ConfigurationManager.Instance.LockHUD; + } + ImGui.PopStyleColor(); + ImGui.PopFont(); + ImGuiHelper.SetTooltip("Unlock HUD"); + + // hide button + ImGui.PushFont(UiBuilder.IconFont); + ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(229f / 255f, 57f / 255f, 57f / 255f, alpha)); + ImGui.SetCursorPos(new Vector2(ImGui.GetWindowWidth() - 88 * _scale, 5 * _scale)); + string hideString = ConfigurationManager.Instance.ShowHUD ? FontAwesomeIcon.Eye.ToIconString() : FontAwesomeIcon.EyeSlash.ToIconString(); + if (ImGui.Button(hideString, new Vector2(26 * _scale, 22 * _scale))) + { + ConfigurationManager.Instance.ShowHUD = !ConfigurationManager.Instance.ShowHUD; + } + ImGui.PopStyleColor(); + ImGui.PopFont(); + ImGuiHelper.SetTooltip(ConfigurationManager.Instance.ShowHUD ? "Hide HUD" : "Show HUD"); + + PopStyles(popColors); + + if (didReset) + { + ConfigObjectResetEvent?.Invoke(this); + } + + NeedsSave |= changed | didReset; + } + + public ConfigPageNode? GetOrAddConfig() where T : PluginConfigObject + { + object[] attributes = typeof(T).GetCustomAttributes(true); + + foreach (object attribute in attributes) + { + if (attribute is SectionAttribute sectionAttribute) + { + foreach (SectionNode sectionNode in _children) + { + if (sectionNode.Name == sectionAttribute.SectionName) + { + return sectionNode.GetOrAddConfig(); + } + } + + SectionNode newNode = new(); + newNode.Name = sectionAttribute.SectionName; + newNode.ForceAllowExport = sectionAttribute.ForceAllowExport; + _children.Add(newNode); + + return newNode.GetOrAddConfig(); + } + } + + Type type = typeof(T); + throw new ArgumentException("The provided configuration object does not specify a section: " + type.Name); + } + } +} diff --git a/Config/Tree/ConfigPageNode.cs b/Config/Tree/ConfigPageNode.cs new file mode 100644 index 0000000..1cecc78 --- /dev/null +++ b/Config/Tree/ConfigPageNode.cs @@ -0,0 +1,311 @@ +using Dalamud.Logging; +using HSUI.Config.Attributes; +using HSUI.Config.Profiles; +using HSUI.Helpers; +using HSUI.Interface; +using Dalamud.Bindings.ImGui; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Numerics; +using System.Reflection; + +namespace HSUI.Config.Tree +{ + public class ConfigPageNode : SubSectionNode + { + private PluginConfigObject _configObject = null!; + private List? _drawList = null; + private Dictionary _nestedConfigPageNodes = null!; + + public PluginConfigObject ConfigObject + { + get => _configObject; + set + { + _configObject = value; + GenerateNestedConfigPageNodes(); + _drawList = null; + } + } + + public override List GetObjects() + { + return _configObject.GetObjects(); + } + + private void GenerateNestedConfigPageNodes() + { + _nestedConfigPageNodes = new Dictionary(); + + FieldInfo[] fields = _configObject.GetType().GetFields(); + + foreach (var field in fields) + { + foreach (var attribute in field.GetCustomAttributes(true)) + { + if (attribute is not NestedConfigAttribute nestedConfigAttribute) + { + continue; + } + + var value = field.GetValue(_configObject); + + if (value is not PluginConfigObject nestedConfig) + { + continue; + } + + ConfigPageNode configPageNode = new(); + configPageNode.ConfigObject = nestedConfig; + configPageNode.Name = nestedConfigAttribute.friendlyName; + + if (nestedConfig.Disableable) + { + configPageNode.Name += "##" + nestedConfig.GetHashCode(); + } + + _nestedConfigPageNodes.Add(field.Name, configPageNode); + } + } + } + + public override string? GetBase64String() + { + if (!AllowShare()) + { + return null; + } + + return ImportExportHelper.GenerateExportString(ConfigObject); + } + + protected override bool AllowExport() + { + return ConfigObject.Exportable; + } + + protected override bool AllowShare() + { + return ConfigObject.Shareable; + } + + protected override bool AllowReset() + { + return ConfigObject.Resettable; + } + + public override bool Draw(ref bool changed) { return DrawWithID(ref changed); } + + private bool DrawWithID(ref bool changed, string? ID = null) + { + bool didReset = false; + + // Only do this stuff the first time the config page is loaded + if (_drawList is null) + { + _drawList = GenerateDrawList(); + } + + if (_drawList is not null) + { + foreach (var fieldNode in _drawList) + { + didReset |= fieldNode.Draw(ref changed); + } + } + + didReset |= DrawPortableSection(); + + ImGui.NewLine(); // fixes some long pages getting cut off + + return didReset; + } + + private List GenerateDrawList(string? ID = null) + { + Dictionary fieldMap = new Dictionary(); + + FieldInfo[] fields = ConfigObject.GetType().GetFields(); + foreach (var field in fields) + { + if (ConfigObject.DisableParentSettings != null && ConfigObject.DisableParentSettings.Contains(field.Name)) + { + continue; + } + + foreach (object attribute in field.GetCustomAttributes(true)) + { + if (attribute is NestedConfigAttribute nestedConfigAttribute && _nestedConfigPageNodes.TryGetValue(field.Name, out ConfigPageNode? node)) + { + var newNodes = node.GenerateDrawList(node.Name); + foreach (var newNode in newNodes) + { + newNode.Position = nestedConfigAttribute.pos; + newNode.Separator = nestedConfigAttribute.separator; + newNode.Spacing = nestedConfigAttribute.spacing; + newNode.ParentName = nestedConfigAttribute.collapseWith; + newNode.Nest = nestedConfigAttribute.nest; + newNode.CollapsingHeader = nestedConfigAttribute.collapsingHeader; + fieldMap.Add($"{node.Name}_{newNode.Name}", newNode); + } + } + else if (attribute is OrderAttribute orderAttribute) + { + var fieldNode = new FieldNode(field, ConfigObject, ID); + fieldNode.Position = orderAttribute.pos; + fieldNode.ParentName = orderAttribute.collapseWith; + if (fieldMap.TryGetValue(field.Name, out var existing) && existing is FieldNode existingFn && + existingFn.Field.DeclaringType != null && field.DeclaringType != null && + existingFn.Field.DeclaringType.IsAssignableFrom(field.DeclaringType) && + existingFn.Field.DeclaringType != field.DeclaringType) + { + fieldMap[field.Name] = fieldNode; + } + else if (!fieldMap.ContainsKey(field.Name)) + { + fieldMap.Add(field.Name, fieldNode); + } + } + } + } + + var manualDrawMethods = ConfigObject.GetType().GetMethods().Where(m => Attribute.IsDefined(m, typeof(ManualDrawAttribute), false)); + foreach (var method in manualDrawMethods) + { + string id = $"ManualDraw##{method.GetHashCode()}"; + fieldMap.Add(id, new ManualDrawNode(method, ConfigObject, id)); + } + + foreach (var configNode in fieldMap.Values) + { + if (configNode.ParentName is not null && + fieldMap.TryGetValue(configNode.ParentName, out ConfigNode? parentNode)) + { + if (!ConfigObject.Disableable && + parentNode.Name.Equals("Enabled") && + parentNode.ID is null) + { + continue; + } + + if (parentNode is FieldNode parentFieldNode) + { + parentFieldNode.CollapseControl = true; + parentFieldNode.AddChild(configNode.Position, configNode); + } + } + } + + var fieldNodes = fieldMap.Values.ToList(); + fieldNodes.RemoveAll(f => f.IsChild); + fieldNodes.Sort((x, y) => x.Position - y.Position); + return fieldNodes; + } + + private bool DrawPortableSection() + { + if (!AllowExport()) + { + return false; + } + + ImGuiHelper.DrawSeparator(2, 1); + + const float buttonWidth = 120; + + ImGui.BeginGroup(); + + float width = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; + ImGui.SetCursorPos(new Vector2(width / 2f - buttonWidth - 5, ImGui.GetCursorPosY())); + + if (ImGui.Button("Export", new Vector2(120, 24))) + { + var exportString = ImportExportHelper.GenerateExportString(ConfigObject); + ImGui.SetClipboardText(exportString); + } + + ImGui.SameLine(); + + if (ImGui.Button("Reset", new Vector2(120, 24))) + { + _nodeToReset = this; + _nodeToResetName = Utils.UserFriendlyConfigName(ConfigObject.GetType().Name); + } + + ImGui.NewLine(); + ImGui.EndGroup(); + + return DrawResetModal(); + } + + public override void Save(string path) + { + string[] splits = path.Split("\\", StringSplitOptions.RemoveEmptyEntries); + string directory = path.Replace(splits.Last(), ""); + Directory.CreateDirectory(directory); + + string finalPath = path + ".json"; + + try + { + File.WriteAllText( + finalPath, + JsonConvert.SerializeObject( + ConfigObject, + Formatting.Indented, + new JsonSerializerSettings { TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple, TypeNameHandling = TypeNameHandling.Objects } + ) + ); + } + catch (Exception e) + { + Plugin.Logger.Error("Error when saving config object: " + e.Message); + } + } + + public override void Load(string path) + { + if (ConfigObject is not PluginConfigObject) { return; } + + FileInfo finalPath = new(path + ".json"); + + // Use reflection to call the LoadForType method, this allows us to specify a type at runtime. + // While in general use this is important as the conversion from the superclass 'PluginConfigObject' to a specific subclass (e.g. 'BlackMageHudConfig') would + // be handled by Json.NET, when the plugin is reloaded with a different assembly (as is the case when using LivePluginLoader, or updating the plugin in-game) + // it fails. In order to fix this we need to specify the specific subclass, in order to do this during runtime we must use reflection to set the generic. + MethodInfo? methodInfo = ConfigObject.GetType().GetMethod("Load"); + MethodInfo? function = methodInfo?.MakeGenericMethod(ConfigObject.GetType()); + + object?[] args = new object?[] { finalPath }; + PluginConfigObject? config = (PluginConfigObject?)function?.Invoke(ConfigObject, args); + + ConfigObject = config ?? ConfigObject; + } + + public override void Reset() + { + Type type = ConfigObject.GetType(); + ImportData? importData = ProfilesManager.Instance?.DefaultImportData(type); + + if (importData == null) + { + Plugin.Logger.Error("Error finding default import data for type " + type.ToString()); + return; + } + + PluginConfigObject? config = importData.GetObject(); + if (config == null) + { + Plugin.Logger.Error("Error importing default import data for type " + type.ToString()); + return; + } + + ConfigObject = config; + } + + public override ConfigPageNode? GetOrAddConfig() => this; + } +} \ No newline at end of file diff --git a/Config/Tree/FieldNode.cs b/Config/Tree/FieldNode.cs new file mode 100644 index 0000000..4bb1988 --- /dev/null +++ b/Config/Tree/FieldNode.cs @@ -0,0 +1,214 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Reflection; +using HSUI.Config.Attributes; +using HSUI.Helpers; +using Dalamud.Bindings.ImGui; + +namespace HSUI.Config.Tree +{ + public abstract class ConfigNode + { + public bool CollapseControl { get; set; } + + public bool IsChild { get; set; } + + public string Name { get; private set; } + + public bool Nest { get; set; } = true; + + public string? ParentName { get; set; } + + public int Position { get; set; } = Int32.MaxValue; + + public bool Separator { get; set; } + + public bool Spacing { get; set; } + + public bool CollapsingHeader { get; set; } + + public string? ID { get; private set; } + + protected PluginConfigObject ConfigObject { get; set; } + + public ConfigNode(PluginConfigObject configObject, string? id, string name) + { + ConfigObject = configObject; + ID = id; + Name = name; + } + + public abstract bool Draw(ref bool changed, int depth = 0); + + protected void DrawSeparatorOrSpacing() + { + if (Separator) + { + ImGuiHelper.DrawSeparator(1, 1); + } + + if (Spacing) + { + ImGuiHelper.DrawSpacing(1); + } + } + + protected static void DrawNestIndicator(int depth) + { + // This draws the L shaped symbols and padding to the left of config items collapsible under a checkbox. + // Shift cursor to the right to pad for children with depth more than 1. + // 26 is an arbitrary value I found to be around half the width of a checkbox + ImGui.SetCursorPos(ImGui.GetCursorPos() + new Vector2(26, 0) * Math.Max((depth - 1), 0)); + + if (ConfigurationManager.Instance.OverrideDalamudStyle) + { + ImGui.TextColored(new Vector4(229f / 255f, 57f / 255f, 57f / 255f, 1f), "\u2514"); + } + else + { + ImGui.Text("\u2514"); + } + + ImGui.SameLine(); + } + + protected static ConfigAttribute? GetConfigAttribute(FieldInfo field) + { + return field.GetCustomAttributes(true).Where(a => a is ConfigAttribute).FirstOrDefault() as ConfigAttribute; + } + } + + public class FieldNode : ConfigNode + { + private SortedDictionary _childNodes; + private ConfigAttribute? _configAttribute; + private FieldInfo _mainField; + + public FieldInfo Field => _mainField; + + public FieldNode(FieldInfo mainField, PluginConfigObject configObject, string? id) : base(configObject, id, mainField.Name) + { + _mainField = mainField; + _childNodes = new SortedDictionary(); + + _configAttribute = GetConfigAttribute(mainField); + if (_configAttribute is not null) + { + Separator = _configAttribute.separator; + Spacing = _configAttribute.spacing; + } + } + + public void AddChild(int position, ConfigNode field) + { + field.IsChild = true; + + while (_childNodes.ContainsKey(position)) + { + position++; + } + + _childNodes.Add(position, field); + } + + public override bool Draw(ref bool changed, int depth = 0) + { + bool reset = false; + DrawSeparatorOrSpacing(); + + if (!Nest) + { + depth = 0; + } + + if (depth > 0) + { + DrawNestIndicator(depth); + } + + bool collapsing = CollapsingHeader && ConfigObject.Disableable; + + // Draw the ConfigAttribute + if (!collapsing) + { + DrawConfigAttribute(ref changed, _mainField); + } + + bool enabled = _mainField.GetValue(ConfigObject) as bool? ?? false; + + // Draw children + if (CollapseControl && Attribute.IsDefined(_mainField, typeof(CheckboxAttribute))) + { + if (collapsing) + { + if (ImGui.CollapsingHeader(ID + "##CollapsingHeader")) + { + DrawNestIndicator(depth); + DrawConfigAttribute(ref changed, _mainField); + + if (enabled) + { + reset |= DrawChildren(ref changed, depth); + } + } + } + else if (!collapsing && enabled) + { + ImGui.BeginGroup(); + reset |= DrawChildren(ref changed, depth); + ImGui.EndGroup(); + } + } + + return reset; + } + + private bool DrawChildren(ref bool changed, int depth) + { + bool reset = false; + + int childDepth = depth + 1; + foreach (ConfigNode child in _childNodes.Values) + { + if (child.Separator) + { + childDepth = 0; + } + + reset |= child.Draw(ref changed, childDepth); + } + + return reset; + } + + private void DrawConfigAttribute(ref bool changed, FieldInfo field) + { + if (_configAttribute is not null) + { + changed |= _configAttribute.Draw(field, ConfigObject, ID, CollapsingHeader); + } + } + } + + public class ManualDrawNode : ConfigNode + { + private MethodInfo _drawMethod; + + public ManualDrawNode(MethodInfo method, PluginConfigObject configObject, string? id) : base(configObject, id, id ?? "") + { + _drawMethod = method; + } + + public override bool Draw(ref bool changed, int depth = 0) + { + object[] args = new object[] { false }; + bool? result = (bool?)_drawMethod.Invoke(ConfigObject, args); + + bool arg = (bool)args[0]; + changed |= arg; + return result ?? false; + } + } +} \ No newline at end of file diff --git a/Config/Tree/Node.cs b/Config/Tree/Node.cs new file mode 100644 index 0000000..f620ce4 --- /dev/null +++ b/Config/Tree/Node.cs @@ -0,0 +1,164 @@ +using HSUI.Helpers; +using System.Collections.Generic; + +namespace HSUI.Config.Tree +{ + public abstract class Node + { + protected List _children = new List(); + public IReadOnlyList Children => _children.AsReadOnly(); + + public void Add(Node node) + { + _children.Add(node); + } + + #region reset + protected Node? _nodeToReset = null; + protected string? _nodeToResetName = null; + + public virtual List GetObjects() + { + List list = new List(); + + foreach (Node node in _children) + { + list.AddRange(node.GetObjects()); + } + + return list; + } + + protected void DrawExportResetContextMenu(Node node, string name) + { + if (_nodeToReset != null) + { + return; + } + + bool allowExport = node.AllowExport(); + bool allowReset = node.AllowReset(); + if (!allowExport && !allowReset) + { + return; + } + + _nodeToReset = ImGuiHelper.DrawExportResetContextMenu(node, allowExport, allowReset); + _nodeToResetName = name; + } + + protected virtual bool AllowExport() + { + foreach (Node child in _children) + { + if (child.AllowExport()) + { + return true; + } + } + + return false; + } + + protected virtual bool AllowShare() + { + foreach (Node child in _children) + { + if (child.AllowShare()) + { + return true; + } + } + + return false; + } + + protected virtual bool AllowReset() + { + foreach (Node child in _children) + { + if (child.AllowReset()) + { + return true; + } + } + + return false; + } + + protected bool DrawResetModal() + { + if (_nodeToReset == null || _nodeToResetName == null) + { + return false; + } + + string[] lines = new string[] { "Are you sure you want to reset \"" + _nodeToResetName + "\"?" }; + var (didReset, didClose) = ImGuiHelper.DrawConfirmationModal("Reset?", lines); + + if (didReset) + { + _nodeToReset.Reset(); + _nodeToReset = null; + } + else if (didClose) + { + _nodeToReset = null; + } + + return didReset; + } + + + public virtual void Reset() + { + foreach (Node child in _children) + { + child.Reset(); + } + } + #endregion + + #region save and load + public virtual void Save(string path) + { + foreach (Node child in _children) + { + child.Save(path); + } + } + + public virtual void Load(string path) + { + foreach (Node child in _children) + { + child.Load(path); + } + } + #endregion + + #region export + public virtual string? GetBase64String() + { + if (_children == null) + { + return ""; + } + + string base64String = ""; + + foreach (Node child in _children) + { + string? childString = child.GetBase64String(); + + if (childString != null && childString.Length > 0) + { + base64String += "|" + childString; + } + } + + return base64String; + } + #endregion + } +} diff --git a/Config/Tree/SectionNode.cs b/Config/Tree/SectionNode.cs new file mode 100644 index 0000000..0034f4b --- /dev/null +++ b/Config/Tree/SectionNode.cs @@ -0,0 +1,147 @@ +using HSUI.Config.Attributes; +using Dalamud.Bindings.ImGui; +using System; +using System.IO; +using System.Numerics; + +namespace HSUI.Config.Tree +{ + public class SectionNode : Node + { + public bool Selected; + public string Name = null!; + public bool ForceAllowExport = false; + public string? ForceSelectedTabName = null; + + public SectionNode() { } + + protected override bool AllowExport() + { + if (ForceAllowExport) { return true; } + + return base.AllowExport(); + } + + public bool Draw(ref bool changed, float alpha) + { + if (!Selected) + { + return false; + } + + bool didReset = false; + + ImGui.NewLine(); + + if (ImGui.BeginChild( + "DelvU_Settings_Tab", + new Vector2(0, -10), + false, + ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse + )) + { + bool popColors = false; + if (ConfigurationManager.Instance.OverrideDalamudStyle) + { + ImGui.PushStyleColor(ImGuiCol.Tab, new Vector4(45f / 255f, 45f / 255f, 45f / 255f, alpha)); + ImGui.PushStyleColor(ImGuiCol.Button, new Vector4(45f / 255f, 45f / 255f, 45f / 255f, alpha)); + popColors = true; + } + + if (ImGui.BeginTabBar("##Tabs", ImGuiTabBarFlags.None)) + { + foreach (SubSectionNode subSectionNode in _children) + { + if (ForceSelectedTabName != null) + { + bool a = subSectionNode.Name == ForceSelectedTabName; // no idea how this works + ImGuiTabItemFlags flag = subSectionNode.Name == ForceSelectedTabName ? ImGuiTabItemFlags.SetSelected : ImGuiTabItemFlags.None; + + if (!ImGui.BeginTabItem(subSectionNode.Name, ref a, flag)) + { + continue; + } + } + else + { + if (!ImGui.BeginTabItem(subSectionNode.Name)) + { + continue; + } + } + + DrawExportResetContextMenu(subSectionNode, subSectionNode.Name); + + ImGui.BeginChild("subconfig value", new Vector2(0, 0), true); + didReset |= subSectionNode.Draw(ref changed); + ImGui.EndChild(); + + ImGui.EndTabItem(); + } + + ImGui.EndTabBar(); + ForceSelectedTabName = null; + } + + if (popColors) + { + ImGui.PopStyleColor(2); + } + + didReset |= DrawResetModal(); + } + + ImGui.EndChild(); + + return didReset; + } + + public override void Save(string path) + { + foreach (SubSectionNode child in _children) + { + child.Save(Path.Combine(path, Name)); + } + } + + public override void Load(string path) + { + foreach (SubSectionNode child in _children) + { + child.Load(Path.Combine(path, Name)); + } + } + + public ConfigPageNode? GetOrAddConfig() where T : PluginConfigObject + { + object[] attributes = typeof(T).GetCustomAttributes(true); + + foreach (object attribute in attributes) + { + if (attribute is SubSectionAttribute subSectionAttribute) + { + foreach (SubSectionNode subSectionNode in _children) + { + if (subSectionNode.Name == subSectionAttribute.SubSectionName) + { + return subSectionNode.GetOrAddConfig(); + } + } + + if (subSectionAttribute.Depth == 0) + { + NestedSubSectionNode newNode = new(); + newNode.Name = subSectionAttribute.SubSectionName; + newNode.Depth = 0; + _children.Add(newNode); + + return newNode.GetOrAddConfig(); + } + } + } + + Type type = typeof(T); + throw new ArgumentException("The provided configuration object does not specify a sub-section: " + type.Name); + } + } +} diff --git a/Config/Tree/SubSectionNode.cs b/Config/Tree/SubSectionNode.cs new file mode 100644 index 0000000..d3460e6 --- /dev/null +++ b/Config/Tree/SubSectionNode.cs @@ -0,0 +1,173 @@ +using HSUI.Config.Attributes; +using Dalamud.Bindings.ImGui; +using System.IO; +using System.Numerics; +using System.Reflection; + +namespace HSUI.Config.Tree +{ + public abstract class SubSectionNode : Node + { + public string Name = null!; + public int Depth; + public string? ForceSelectedTabName = null; + + public abstract bool Draw(ref bool changed); + + public abstract ConfigPageNode? GetOrAddConfig() where T : PluginConfigObject; + } + + public class NestedSubSectionNode : SubSectionNode + { + public NestedSubSectionNode() { } + + public override bool Draw(ref bool changed) + { + bool didReset = false; + + if (_children.Count > 1) + { + ImGui.BeginChild( + "DelvUI_Tabs_" + Depth, + new Vector2(0, ImGui.GetWindowHeight() - 22), + false, + ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse + ); // Leave room for 1 line below us + + if (ImGui.BeginTabBar("##tabs" + Depth, ImGuiTabBarFlags.None)) + { + didReset |= DrawSubConfig(ref changed); + } + + ImGui.EndTabBar(); + + ImGui.EndChild(); + } + else + { + ImGui.BeginChild("item" + Depth + " view", new Vector2(0, ImGui.GetWindowHeight() - 20)); // Leave room for 1 line below us + + didReset |= DrawSubConfig(ref changed); + + ImGui.EndChild(); + } + + return didReset; + } + + public bool DrawSubConfig(ref bool changed) + { + bool didReset = false; + + foreach (SubSectionNode subSectionNode in _children) + { + if (subSectionNode is NestedSubSectionNode) + { + if (ForceSelectedTabName != null) + { + bool a = subSectionNode.Name == ForceSelectedTabName; // no idea how this works + ImGuiTabItemFlags flag = subSectionNode.Name == ForceSelectedTabName ? ImGuiTabItemFlags.SetSelected : ImGuiTabItemFlags.None; + + if (!ImGui.BeginTabItem(subSectionNode.Name, ref a, flag)) + { + continue; + } + } + else + { + if (!ImGui.BeginTabItem(subSectionNode.Name)) + { + continue; + } + } + + DrawExportResetContextMenu(subSectionNode, subSectionNode.Name); + + ImGui.BeginChild("subconfig" + Depth + " value", new Vector2(0, ImGui.GetWindowHeight())); + didReset |= subSectionNode.Draw(ref changed); + ImGui.EndChild(); + + ImGui.EndTabItem(); + } + else + { + didReset |= subSectionNode.Draw(ref changed); + } + } + + ForceSelectedTabName = null; + + didReset |= DrawResetModal(); + + return didReset; + } + + public override void Save(string path) + { + foreach (SubSectionNode child in _children) + { + child.Save(Path.Combine(path, Name)); + } + } + + public override void Load(string path) + { + foreach (SubSectionNode child in _children) + { + child.Load(Path.Combine(path, Name)); + } + } + + public override ConfigPageNode? GetOrAddConfig() + { + var type = typeof(T); + if (type == null) + { + return null; + } + + object[] attributes = type.GetCustomAttributes(true); + + foreach (object attribute in attributes) + { + if (attribute is SubSectionAttribute subSectionAttribute) + { + if (subSectionAttribute.Depth != Depth + 1) + { + continue; + } + + foreach (SubSectionNode subSectionNode in _children) + { + if (subSectionNode.Name == subSectionAttribute.SubSectionName) + { + return subSectionNode.GetOrAddConfig(); + } + } + + NestedSubSectionNode nestedSubSectionNode = new(); + nestedSubSectionNode.Name = subSectionAttribute.SubSectionName; + nestedSubSectionNode.Depth = Depth + 1; + _children.Add(nestedSubSectionNode); + + return nestedSubSectionNode.GetOrAddConfig(); + } + } + + foreach (SubSectionNode subSectionNode in _children) + { + if (subSectionNode.Name == type.FullName && subSectionNode is ConfigPageNode node) + { + return node; + } + } + + ConfigPageNode configPageNode = new(); + configPageNode.ConfigObject = ConfigurationManager.GetDefaultConfigObjectForType(type); + configPageNode.Name = type.FullName!; + _children.Add(configPageNode); + + return configPageNode; + } + } +} diff --git a/Config/Windows/ChangelogWindow.cs b/Config/Windows/ChangelogWindow.cs new file mode 100644 index 0000000..cc24b7f --- /dev/null +++ b/Config/Windows/ChangelogWindow.cs @@ -0,0 +1,77 @@ +using Dalamud.Interface.Windowing; +using HSUI.Helpers; +using Dalamud.Bindings.ImGui; +using System; +using System.Numerics; + +namespace HSUI.Config.Windows +{ + public class ChangelogWindow : Window + { + public string Changelog { get; set; } + private bool _needsToSetSize = true; + + public bool AutoClose = false; + private double _openTime = -1; + + private bool _popColors = false; + + public ChangelogWindow(string name, string changelog) : base(name) + { + Changelog = changelog; + } + + public override void PreDraw() + { + if (ConfigurationManager.Instance.OverrideDalamudStyle) + { + ImGui.PushStyleColor(ImGuiCol.WindowBg, new Vector4(10f / 255f, 10f / 255f, 10f / 255f, 0.95f)); + _popColors = true; + } + + if (_needsToSetSize) + { + float height = ImGui.CalcTextSize(Changelog).Y + 100; + ImGui.SetNextWindowSize(new Vector2(500, Math.Min(height, 500)), ImGuiCond.FirstUseEver); + _needsToSetSize = false; + } + } + + public override void Draw() + { + Vector2 size = ImGui.GetWindowSize(); + ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + size.X - 24); + ImGui.TextWrapped(Changelog); + + if (AutoClose && + _openTime > 0 && + ImGui.GetTime() - _openTime > 10) + { + IsOpen = false; + } + } + + public override void PostDraw() + { + if (_popColors) + { + ImGui.PopStyleColor(); + _popColors = false; + } + } + + public override void OnOpen() + { + _openTime = ImGui.GetTime(); + } + + public override void OnClose() + { + if (AutoClose && InputsHelper.Instance != null) + { + AutoClose = false; + Plugin.LoadTime = ImGui.GetTime() - InputsHelper.InitializationDelay; + } + } + } +} diff --git a/Config/Windows/GridWindow.cs b/Config/Windows/GridWindow.cs new file mode 100644 index 0000000..7a1079f --- /dev/null +++ b/Config/Windows/GridWindow.cs @@ -0,0 +1,56 @@ +using Dalamud.Interface.Windowing; +using HSUI.Interface.GeneralElements; +using Dalamud.Bindings.ImGui; +using System.Numerics; + +namespace HSUI.Config.Windows +{ + public class GridWindow : Window + { + private bool _popColors = false; + public GridWindow(string name) : base(name) + { + Flags = ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoScrollWithMouse; + Size = new Vector2(300, 200); + } + + public override void OnClose() + { + ConfigurationManager.Instance.LockHUD = true; + } + + public override void PreDraw() + { + if (ConfigurationManager.Instance.OverrideDalamudStyle) + { + ImGui.PushStyleColor(ImGuiCol.WindowBg, new Vector4(10f / 255f, 10f / 255f, 10f / 255f, 0.95f)); + _popColors = true; + } + + ImGui.SetNextWindowFocus(); + } + + public override void Draw() + { + var configManager = ConfigurationManager.Instance; + var node = configManager.GetConfigPageNode(); + if (node == null) + { + return; + } + + ImGui.PushItemWidth(150); + bool changed = false; + node.Draw(ref changed); + } + + public override void PostDraw() + { + if (_popColors) + { + ImGui.PopStyleColor(); + _popColors = false; + } + } + } +} diff --git a/Config/Windows/MainConfigWindow.cs b/Config/Windows/MainConfigWindow.cs new file mode 100644 index 0000000..2b7897f --- /dev/null +++ b/Config/Windows/MainConfigWindow.cs @@ -0,0 +1,82 @@ +using Dalamud.Interface.Windowing; +using Dalamud.Logging; +using HSUI.Config.Tree; +using HSUI.Interface.GeneralElements; +using Dalamud.Bindings.ImGui; +using System; +using System.Numerics; + +namespace HSUI.Config.Windows +{ + public class MainConfigWindow : Window + { + public BaseNode? node { get; set; } + public Action? CloseAction; + + private float _alpha = 1f; + private Vector2 _lastWindowPos = Vector2.Zero; + private Vector2 _size = new Vector2(1050, 750); + + private bool _popColors = false; + + public MainConfigWindow(string name) : base(name) + { + Flags = ImGuiWindowFlags.NoTitleBar; + Size = _size; + SizeCondition = ImGuiCond.FirstUseEver; + } + + public override void OnClose() + { + CloseAction?.Invoke(); + } + + private bool CheckWindowFocus() + { + Vector2 mousePos = ImGui.GetMousePos(); + Vector2 endPos = _lastWindowPos + _size; + + return mousePos.X >= _lastWindowPos.X && mousePos.X <= endPos.X && + mousePos.Y >= _lastWindowPos.Y && mousePos.Y <= endPos.Y; + } + + public override void PreDraw() + { + _alpha = 1; + + HUDOptionsConfig? config = ConfigurationManager.Instance.GetConfigObject(); + if (config?.DimConfigWindow == true) + { + _alpha = CheckWindowFocus() ? 1 : 0.5f; + } + + if (ConfigurationManager.Instance.OverrideDalamudStyle) + { + ImGui.PushStyleColor(ImGuiCol.Border, new Vector4(0f / 255f, 0f / 255f, 0f / 255f, _alpha)); + ImGui.PushStyleColor(ImGuiCol.BorderShadow, new Vector4(0f / 255f, 0f / 255f, 0f / 255f, _alpha)); + ImGui.PushStyleColor(ImGuiCol.WindowBg, new Vector4(20f / 255f, 21f / 255f, 20f / 255f, _alpha)); + _popColors = true; + } + + ImGui.PushStyleVar(ImGuiStyleVar.WindowBorderSize, 1); + ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 1); + } + + public override void Draw() + { + _lastWindowPos = ImGui.GetWindowPos(); + + if (_popColors) + { + ImGui.PopStyleColor(3); + _popColors = false; + } + + ImGui.PopStyleVar(2); + + node?.Draw(_alpha); + + _size = ImGui.GetWindowSize(); + } + } +} diff --git a/Enums/BarTextureDrawMode.cs b/Enums/BarTextureDrawMode.cs new file mode 100644 index 0000000..50590b7 --- /dev/null +++ b/Enums/BarTextureDrawMode.cs @@ -0,0 +1,10 @@ +namespace HSUI.Enums +{ + public enum BarTextureDrawMode + { + Stretch = 0, + RepeatHorizontal = 1, + RepeatVertical = 2, + Repeat = 3 + } +} diff --git a/Enums/BlendMode.cs b/Enums/BlendMode.cs new file mode 100644 index 0000000..d63c41f --- /dev/null +++ b/Enums/BlendMode.cs @@ -0,0 +1,14 @@ +namespace HSUI.Enums +{ + public enum BlendMode + { + LAB, + LChab, + XYZ, + RGB, + LChuv, + Luv, + Jzazbz, + JzCzhz + } +} diff --git a/Enums/DamageType.cs b/Enums/DamageType.cs new file mode 100644 index 0000000..dcc4bcd --- /dev/null +++ b/Enums/DamageType.cs @@ -0,0 +1,14 @@ +namespace HSUI.Enums +{ + public enum DamageType + { + Unknown = 0, + Slashing = 1, + Piercing = 2, + Blunt = 3, + Magic = 5, + Darkness = 6, + Physical = 7, + LimitBreak = 8 + } +} diff --git a/Enums/DrawAnchor.cs b/Enums/DrawAnchor.cs new file mode 100644 index 0000000..31c1eea --- /dev/null +++ b/Enums/DrawAnchor.cs @@ -0,0 +1,15 @@ +namespace HSUI.Enums +{ + public enum DrawAnchor + { + Center = 0, + Left = 1, + Right = 2, + Top = 3, + TopLeft = 4, + TopRight = 5, + Bottom = 6, + BottomLeft = 7, + BottomRight = 8 + } +} diff --git a/Enums/ElementKind.cs b/Enums/ElementKind.cs new file mode 100644 index 0000000..aa8a0a2 --- /dev/null +++ b/Enums/ElementKind.cs @@ -0,0 +1,58 @@ +using System; + +namespace HSUI.Enums; + +// Hashes from HUD layout (AddonNameHash). Sources: HSUI, HUDManager plugin +public enum ElementKind : uint +{ + Hotbar1 = 0xC48D3605, // _ActionBar_a + Hotbar2 = 0xFB7B6E1E, // _ActionBar01_a + Hotbar3 = 0xF93DD047, // _ActionBar02_a + Hotbar4 = 0xF8FFBA70, // _ActionBar03_a + Hotbar5 = 0xFDB0ACF5, // _ActionBar04_a + Hotbar6 = 0xFC72C6C2, // _ActionBar05_a + Hotbar7 = 0xFE34789B, // _ActionBar06_a + Hotbar8 = 0xFFF612AC, // _ActionBar07_a + Hotbar9 = 0xF4AA5591, // _ActionBar08_a + Hotbar10 = 0xF5683FA6, // _ActionBar09_a + PetHotbar = 0xD8D188FF, // _ActionBarEx_a + CrossHotbar = 0xBA81E8D1, // _ActionCross_a + LeftWCrossHotbar = 0x6665735D, // _ActionDoubleCrossL_a + RightWCrossHotbar = 0x70DDFD27, // _ActionDoubleCrossR_a + ParameterBar = 0x9818E23E, // _ParameterWidget - player HP/MP + TargetBar = 0x9139E9FD, // target info + FocusTargetBar = 0xC28E7D1F, + ProgressBar = 0xECB1E391, // cast bar + ExperienceBar = 0x21E32D4E, + LimitGauge = 0xC7A40A8A, + StatusEffects = 0x4A5B6A16, // player buffs/debuffs + StatusInfoEnhancements = 0x1F320624, + StatusInfoEnfeeblements = 0x1E7A7A83, + TargetInfoHp = 0xBD411F17, + TargetInfoProgressBar = 0xCB5BCCCF, + TargetInfoStatus = 0x070F4E6B, + PartyList = 0x3D3B34F9, + EnemyList = 0xB8C30AC5, +} + +public static class ElementKindHelper +{ + public static ElementKind ElementKindByHotBarId(int hotBarId) + { + return hotBarId switch + { + 0 => ElementKind.Hotbar1, + 1 => ElementKind.Hotbar2, + 2 => ElementKind.Hotbar3, + 3 => ElementKind.Hotbar4, + 4 => ElementKind.Hotbar5, + 5 => ElementKind.Hotbar6, + 6 => ElementKind.Hotbar7, + 7 => ElementKind.Hotbar8, + 8 => ElementKind.Hotbar9, + 9 => ElementKind.Hotbar10, + 10 => ElementKind.PetHotbar, + _ => throw new ArgumentOutOfRangeException(nameof(hotBarId)) + }; + } +} \ No newline at end of file diff --git a/Enums/StrataLevel.cs b/Enums/StrataLevel.cs new file mode 100644 index 0000000..9b2432e --- /dev/null +++ b/Enums/StrataLevel.cs @@ -0,0 +1,13 @@ +namespace HSUI.Enums +{ + public enum StrataLevel + { + LOWEST = 0, + LOW, + MID_LOW, + MID, + MID_HIGH, + HIGH, + HIGHEST, + } +} diff --git a/Extensions.cs b/Extensions.cs new file mode 100644 index 0000000..e266ab3 --- /dev/null +++ b/Extensions.cs @@ -0,0 +1,139 @@ +using System; +using System.Globalization; +using System.Numerics; +using System.Text; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Game.Text.SeStringHandling; +using HSUI.Config; +using HSUI.Interface.Bars; +using FFXIVClientStructs.FFXIV.Client.System.String; +using static System.Globalization.CultureInfo; + +namespace HSUI +{ + public static class Extensions + { + public static string Abbreviate(this string str) + { + if (str.Length > 20) { + string[] splits = str.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + for (int i = 0; i < splits.Length - 1; i++) + { + splits[i] = splits[i][0].ToString(); + } + + return string.Join(". ", splits).ToString(); + } + + return str; + } + + public static string FirstName(this string str) + { + string[] splits = str.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + return splits.Length > 0 ? splits[0] : ""; + } + + public static string LastName(this string str) + { + string[] splits = str.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + return splits.Length > 1 ? splits[^1] : ""; + } + + public static string Initials(this string str) + { + var initials = ""; + var firstName = FirstName(str); + var lastName = LastName(str); + + if (firstName.Length > 0) + { + initials = firstName[0] + "."; + } + + if (lastName.Length > 0) + { + initials += " " + lastName[0] + "."; + } + + return initials; + } + + public static string Truncated(this string str, int length = 0) + { + return length > 0 ? str.Substring(0, Math.Min(str.Length, length)) : str; + } + + public static Vector4 AdjustColor(this Vector4 vec, float correctionFactor) + { + float red = vec.X; + float green = vec.Y; + float blue = vec.Z; + + if (correctionFactor < 0) + { + correctionFactor = 1 + correctionFactor; + red *= correctionFactor; + green *= correctionFactor; + blue *= correctionFactor; + } + else + { + red = (1 - red) * correctionFactor + red; + green = (1 - green) * correctionFactor + green; + blue = (1 - blue) * correctionFactor + blue; + } + + return new Vector4(red, green, blue, vec.W); + } + + public static Vector4 WithNewAlpha(this Vector4 vec, float alpha) + { + return new Vector4(vec.X, vec.Y, vec.Z, alpha); + } + + public static string KiloFormat(this uint num) + { + return num switch + { + >= 100000000 => (num / 1000000.0).ToString("#,0M", ConfigurationManager.Instance.ActiveCultreInfo), + >= 1000000 => (num / 1000000.0).ToString("0.0", ConfigurationManager.Instance.ActiveCultreInfo) + "M", + >= 100000 => (num / 1000.0).ToString("#,0K", ConfigurationManager.Instance.ActiveCultreInfo), + >= 10000 => (num / 1000.0).ToString("0.0", ConfigurationManager.Instance.ActiveCultreInfo) + "K", + _ => num.ToString("#,0", ConfigurationManager.Instance.ActiveCultreInfo) + }; + } + + public static bool IsHorizontal(this BarDirection direction) + { + return direction == BarDirection.Right || direction == BarDirection.Left; + } + + public static bool IsInverted(this BarDirection direction) + { + return direction == BarDirection.Left || direction == BarDirection.Up; + } + + public static void Draw(this BarHud[] bars, Vector2 origin) + { + foreach (BarHud bar in bars) + { + bar.Draw(origin); + } + } + + public static string CheckForUpperCase(this string str) + { + var culture = CurrentCulture.TextInfo; + if (!string.IsNullOrEmpty(str) && char.IsLetter(str[0]) && !char.IsUpper(str[0])) + { + str = culture.ToTitleCase(str); + } + + return str; + } + + public static string Repeat(this string s, int n) + => new StringBuilder(s.Length * n).Insert(0, s, n).ToString(); + } +} diff --git a/HSUI.csproj b/HSUI.csproj new file mode 100644 index 0000000..210c21f --- /dev/null +++ b/HSUI.csproj @@ -0,0 +1,56 @@ + + + x64 + net10.0-windows + latest + x64 + Debug;Release + + + + HSUI + 1.0.0.0 + 1.0.0.0 + 1.0.0.0 + + + + Library + true + false + enable + false + + + + true + + + + dev + ../repos/Dalamud-master/ + + + + + $(AssemblySearchPaths); + $(DalamudLocal); + $(DalamudLibPath); + + + + + + + + + + + + + + + + + + diff --git a/HSUI.json b/HSUI.json new file mode 100644 index 0000000..ffbb9fe --- /dev/null +++ b/HSUI.json @@ -0,0 +1,11 @@ +{ + "Author": "Knack117", + "Name": "HSUI", + "InternalName": "HSUI", + "AssemblyVersion": "1.0.0.0", + "Description": "HSUI provides a highly configurable HUD replacement for FFXIV, recreated from DelvUI using KamiToolKit, FFXIVClientStructs, and Dalamud. Features unit frames, castbars, job gauges, nameplates, party frames, status effects, enemy list, configurable hotbars with drag-and-drop, and profiles.", + "ApplicableVersion": "any", + "RepoUrl": "https://github.com/Knack117/HSUI", + "Tags": ["UI", "HUD", "Unit Frames", "Nameplates", "Party Frames", "Hotbars"], + "Punchline": "A modern HUD replacement built for customization." +} diff --git a/Helpers/ActionBarsHitTestHelper.cs b/Helpers/ActionBarsHitTestHelper.cs new file mode 100644 index 0000000..b79aa72 --- /dev/null +++ b/Helpers/ActionBarsHitTestHelper.cs @@ -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 +{ + /// + /// 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. + /// + public static class ActionBarsHitTestHelper + { + /// Returns true if the mouse cursor is over any visible HSUI hotbar. + public static bool IsMouseOverAnyHSUIHotbar() + { + try + { + var hotbarsConfig = ConfigurationManager.Instance?.GetConfigObject(); + if (hotbarsConfig == null || !hotbarsConfig.Enabled) + return false; + + var hudOptions = ConfigurationManager.Instance?.GetConfigObject(); + 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(), + ConfigurationManager.Instance?.GetConfigObject(), + ConfigurationManager.Instance?.GetConfigObject(), + ConfigurationManager.Instance?.GetConfigObject(), + ConfigurationManager.Instance?.GetConfigObject(), + ConfigurationManager.Instance?.GetConfigObject(), + ConfigurationManager.Instance?.GetConfigObject(), + ConfigurationManager.Instance?.GetConfigObject(), + ConfigurationManager.Instance?.GetConfigObject(), + ConfigurationManager.Instance?.GetConfigObject() + }; + + 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); + } + } +} diff --git a/Helpers/ActionBarsManager.cs b/Helpers/ActionBarsManager.cs new file mode 100644 index 0000000..a4cb42f --- /dev/null +++ b/Helpers/ActionBarsManager.cs @@ -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!; + } + + /// + /// Slot data for ImGui drawing. Updated each call from game state. + /// + 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; } + /// Keybind hint from game (user's keybind settings). Empty if unavailable. + 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 ?? ""; + } + } + + /// + /// Reads keybind hint from HotbarSlot. Uses _keybindHint (slot display) then _popUpKeybindHint (trimmed) as fallback. + /// Offsets match RaptureHotbarModule.HotbarSlot: _keybindHint 0xA8, _popUpKeybindHint 0x88. + /// + 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; + } + + /// + /// Reads hotbar slot data from RaptureHotbarModule. Returns up to slotCount slots. + /// hotbarIndex 1-10 maps to StandardHotbars 0-9. + /// + public unsafe List GetSlotData(int hotbarIndex, int slotCount) + { + var list = new List(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; + } + + /// + /// Returns the default game keybind label for a hotbar slot (Hotbar 1: 1,2,...,0,-,=; Bar 2: Ctrl+1..12; etc.). + /// hotbarIndex 1–10, slotIndex 0–11. Used to mirror the default hotbar keybind display. + /// + 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}" + }; + } + + /// + /// Execute a hotbar slot. hotbarIndex 1-10, slotIndex 0-based. + /// + 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; + } + + /// + /// 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. + /// + /// When non-null, receives diagnostic info for logging. + public unsafe bool SwapSlots(int hotbarA, int slotA, int hotbarB, int slotB, Action? 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; + } + + /// Dump HotbarSlot and RaptureMacroModule.Macro memory for a slot (for macro persistence debugging). + /// 1-10 + /// 0-11 + 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"); + } + } + + /// Dump AddonMacro and AgentMacro state (open Macro menu first for best data). + 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 ==="); + } + + /// Dump all hotbar slot CommandType/CommandId to the log for SwapSlots diagnosis. + 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}"); + } + } + + /// Clear a hotbar slot. hotbarIndex 1-10, slotIndex 0-based. + 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; + } + } +} diff --git a/Helpers/BarTexturesManager.cs b/Helpers/BarTexturesManager.cs new file mode 100644 index 0000000..c71db5d --- /dev/null +++ b/Helpers/BarTexturesManager.cs @@ -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(); + ConfigurationManager.Instance.ResetEvent += OnConfigReset; + + ReloadTextures(); + } + + private void OnConfigReset(ConfigurationManager sender) + { + _config = sender.GetConfigObject(); + } + + ~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 _textures = new List(); + public IReadOnlyCollection BarTextures => _textures.AsReadOnly(); + + private List _textureNames = new List(); + public IReadOnlyCollection BarTextureNames => _textureNames.AsReadOnly(); + + private Dictionary _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 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 result = new List(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", ""); + } + } +} diff --git a/Helpers/ClipRectsHelper.cs b/Helpers/ClipRectsHelper.cs new file mode 100644 index 0000000..1a012ba --- /dev/null +++ b/Helpers/ClipRectsHelper.cs @@ -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>(_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))); + } + } +} diff --git a/Helpers/ColorUtils.cs b/Helpers/ColorUtils.cs new file mode 100644 index 0000000..182ac7b --- /dev/null +++ b/Helpers/ColorUtils.cs @@ -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 _rgbToLab = new ConverterBuilder().FromRGB().ToLab().Build(); + private static readonly IColorConverter _labToRgb = new ConverterBuilder().FromLab().ToRGB().Build(); + + private static readonly IColorConverter _rgbToLChab = new ConverterBuilder().FromRGB().ToLChab().Build(); + private static readonly IColorConverter _lchabToRgb = new ConverterBuilder().FromLChab().ToRGB().Build(); + + private static readonly IColorConverter _rgbToXyz = new ConverterBuilder().FromRGB(RGBWorkingSpaces.sRGB).ToXYZ(Illuminants.D65).Build(); + private static readonly IColorConverter _xyzToRgb = new ConverterBuilder().FromXYZ(Illuminants.D65).ToRGB(RGBWorkingSpaces.sRGB).Build(); + + private static readonly IColorConverter _rgbToLChuv = new ConverterBuilder().FromRGB().ToLChuv().Build(); + private static readonly IColorConverter _lchuvToRgb = new ConverterBuilder().FromLChuv().ToRGB().Build(); + + private static readonly IColorConverter _rgbToLuv = new ConverterBuilder().FromRGB().ToLuv().Build(); + private static readonly IColorConverter _luvToRgb = new ConverterBuilder().FromLuv().ToRGB().Build(); + + private static readonly IColorConverter _rgbToJzazbz = new ConverterBuilder().FromRGB().ToJzazbz().Build(); + private static readonly IColorConverter _jzazbzToRgb = new ConverterBuilder().FromJzazbz().ToRGB().Build(); + + private static readonly IColorConverter _rgbToJzCzhz = new ConverterBuilder().FromRGB().ToJzCzhz().Build(); + private static readonly IColorConverter _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; + } + } +} diff --git a/Helpers/DraggablesHelper.cs b/Helpers/DraggablesHelper.cs new file mode 100644 index 0000000..e91cd5e --- /dev/null +++ b/Helpers/DraggablesHelper.cs @@ -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 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 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) + }; + } + } +} diff --git a/Helpers/DrawHelper.cs b/Helpers/DrawHelper.cs new file mode 100644 index 0000000..f06ea5f --- /dev/null +++ b/Helpers/DrawHelper.cs @@ -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(dynamic row, Vector2 position, Vector2 size, bool drawBorder, bool cropIcon, int stackCount = 1) where T : struct, IExcelRow + { + IDalamudTextureWrap texture = TexturesHelper.GetTexture(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(ImDrawListPtr drawList, dynamic row, Vector2 position, Vector2 size, bool drawBorder, bool cropIcon, int stackCount = 1) where T : struct, IExcelRow + { + IDalamudTextureWrap texture = TexturesHelper.GetTexture(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 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 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(); + } + } + } + } +} diff --git a/Helpers/EncryptedStringsHelper.cs b/Helpers/EncryptedStringsHelper.cs new file mode 100644 index 0000000..550caab --- /dev/null +++ b/Helpers/EncryptedStringsHelper.cs @@ -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> map = layoutWorld->RsvMap[0]; + Pointer 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>* RsvMap; + } +} diff --git a/Helpers/ExperienceHelper.cs b/Helpers/ExperienceHelper.cs new file mode 100644 index 0000000..08816b9 --- /dev/null +++ b/Helpers/ExperienceHelper.cs @@ -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 _lazyInstance = new Lazy(() => 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(() => 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; + } + } + } +} diff --git a/Helpers/FontsManager.cs b/Helpers/FontsManager.cs new file mode 100644 index 0000000..1122aa6 --- /dev/null +++ b/Helpers/FontsManager.cs @@ -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(); + ConfigurationManager.Instance.ResetEvent += OnConfigReset; + } + + private void OnConfigReset(ConfigurationManager sender) + { + _config = sender.GetConfigObject(); + } + + ~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 _fonts = new List(); + public IReadOnlyCollection 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(); + ImGuiIOPtr io = ImGui.GetIO(); + ushort[]? ranges = GetCharacterRanges(config, io); + + foreach (KeyValuePair 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(); + } + } +} diff --git a/Helpers/GCDHelper.cs b/Helpers/GCDHelper.cs new file mode 100644 index 0000000..9572d48 --- /dev/null +++ b/Helpers/GCDHelper.cs @@ -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 . +*/ + +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 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; + } + } +} diff --git a/Helpers/HonorificHelper.cs b/Helpers/HonorificHelper.cs new file mode 100644 index 0000000..a948ad3 --- /dev/null +++ b/Helpers/HonorificHelper.cs @@ -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? _getCharacterTitle; + + #region Singleton + private HonorificHelper() + { + _getCharacterTitle = Plugin.PluginInterface.GetIpcSubscriber("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(jsonData ?? string.Empty); + return titleData; + } + catch { } + + return null; + } + } +} diff --git a/Helpers/HudLayoutHashHelper.cs b/Helpers/HudLayoutHashHelper.cs new file mode 100644 index 0000000..da488e6 --- /dev/null +++ b/Helpers/HudLayoutHashHelper.cs @@ -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 _cache = new(); + private static DateTime _lastResolveErrorLog = DateTime.MinValue; + private const double ResolveErrorLogIntervalSeconds = 10.0; + + /// Get AddonNameHash for a layout addon. Addon name is without _a suffix (e.g. "_ParameterWidget"). + 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; + } + } + + /// Dump all HudLayout addon names and their hashes to the log (for debugging). + 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}"); + } + } + } +} diff --git a/Helpers/ImGuiHelper.cs b/Helpers/ImGuiHelper.cs new file mode 100644 index 0000000..cdc8ab5 --- /dev/null +++ b/Helpers/ImGuiHelper.cs @@ -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 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 keys = new List(); + 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; + } + } +} diff --git a/Helpers/InputsHelper.cs b/Helpers/InputsHelper.cs new file mode 100644 index 0000000..632587b --- /dev/null +++ b/Helpers/InputsHelper.cs @@ -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 . +*/ + +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(); + + //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( + // "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( + 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(addr, HandleExecuteSlotById); + _executeSlotByIdHook.Enable(); + Plugin.Logger.Info("[HSUI] ExecuteSlotById hook installed (drag-drop overwrite protection)"); + } + else + { + _executeSlotByIdHook = Plugin.GameInteropProvider.HookFromSignature( + "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? _uiMouseOverActorHook; + + private Hook? _requestActionHook; + private Hook? _executeSlotByIdHook; + + private ExcelSheet? _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(); + } + + //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(); + return hotbarsConfig != null && hotbarsConfig.Enabled; + } + catch { return false; } + } + + private static bool IsActionBarDragDropDebugEnabled() + { + try + { + var configs = ConfigurationManager.Instance?.GetObjects(); + return configs != null && configs.Exists(c => c.DebugDragDrop); + } + catch { return false; } + } + + /// 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). + 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); + } + + /// Suppress ExecuteSlotById when we just placed via drag-drop (game hotbar click goes through this). + 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; } + } + + /// 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). + [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 + } +} diff --git a/Helpers/JobsHelper.cs b/Helpers/JobsHelper.cs new file mode 100644 index 0000000..72abeaa --- /dev/null +++ b/Helpers/JobsHelper.cs @@ -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 _cleanseJobs = new List() + { + 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 _raiseJobs = new List() + { + 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 JobRolesMap = new Dictionary() + { + // 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> JobsByRole = new Dictionary>() + { + // tanks + [JobRoles.Tank] = new List() { + JobIDs.GLA, + JobIDs.MRD, + JobIDs.PLD, + JobIDs.WAR, + JobIDs.DRK, + JobIDs.GNB, + }, + + // healers + [JobRoles.Healer] = new List() + { + JobIDs.CNJ, + JobIDs.WHM, + JobIDs.SCH, + JobIDs.AST, + JobIDs.SGE + }, + + // melee dps + [JobRoles.DPSMelee] = new List() { + 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() + { + JobIDs.ARC, + JobIDs.BRD, + JobIDs.MCH, + JobIDs.DNC, + }, + + // ranged magic dps + [JobRoles.DPSCaster] = new List() + { + JobIDs.THM, + JobIDs.ACN, + JobIDs.BLM, + JobIDs.SMN, + JobIDs.RDM, + JobIDs.BLU, + JobIDs.PCT + }, + + // crafters + [JobRoles.Crafter] = new List() + { + JobIDs.CRP, + JobIDs.BSM, + JobIDs.ARM, + JobIDs.GSM, + JobIDs.LTW, + JobIDs.WVR, + JobIDs.ALC, + JobIDs.CUL, + }, + + // gatherers + [JobRoles.Gatherer] = new List() + { + JobIDs.MIN, + JobIDs.BOT, + JobIDs.FSH, + }, + + // unknown + [JobRoles.Unknown] = new List() + }; + + public static Dictionary JobNames = new Dictionary() + { + // 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 JobFullNames = new Dictionary() + { + // 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 RoleNames = new Dictionary() + { + [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 SpecificDPSIcons = new Dictionary() + { + // 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 ColorizedIconIDs = new Dictionary() + { + // 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 PrimaryResourceTypesByRole = new Dictionary() + { + [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; + } +} diff --git a/Helpers/LastUsedCast.cs b/Helpers/LastUsedCast.cs new file mode 100644 index 0000000..7035762 --- /dev/null +++ b/Helpers/LastUsedCast.cs @@ -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()?.GetRow(CastId); + ActionText = action?.Name.ToString() ?? ""; + DamageType = GetDamageType(action); + _lastUsedAction = action; + + break; + + case ActionType.Mount: + Mount? mount = Plugin.DataManager.GetExcelSheet()?.GetRow(CastId); + ActionText = mount?.Singular.ToString() ?? ""; + DamageType = DamageType.Unknown; + _lastUsedAction = mount; + break; + + case ActionType.EventItem: + case ActionType.Item: + Item? item = Plugin.DataManager.GetExcelSheet()?.GetRow(CastId); + ActionText = item?.Name.ToString() ?? "Using item..."; + DamageType = DamageType.Unknown; + _lastUsedAction = item; + break; + + case ActionType.Companion: + Companion? companion = Plugin.DataManager.GetExcelSheet()?.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(_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; + } + } +} diff --git a/Helpers/LayoutHelper.cs b/Helpers/LayoutHelper.cs new file mode 100644 index 0000000..e36037f --- /dev/null +++ b/Helpers/LayoutHelper.cs @@ -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 DirectionOptionsValues = new List() + { + 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) CalculateIconPositions( + GrowthDirections directions, + uint count, + Vector2 position, + Vector2 size, + Vector2 iconSize, + Vector2 iconPadding, + bool fillRowsFirst, + LayoutInfo layoutInfo) + { + List list = new List(); + 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, + } +} diff --git a/Helpers/MpTickHelper.cs b/Helpers/MpTickHelper.cs new file mode 100644 index 0000000..d3e6943 --- /dev/null +++ b/Helpers/MpTickHelper.cs @@ -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 . +*/ + +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); + } + } +} diff --git a/Helpers/PetRenamerHelper.cs b/Helpers/PetRenamerHelper.cs new file mode 100644 index 0000000..32b6132 --- /dev/null +++ b/Helpers/PetRenamerHelper.cs @@ -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? 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()); + } + 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); + } + } +} diff --git a/Helpers/PullTimerHelper.cs b/Helpers/PullTimerHelper.cs new file mode 100644 index 0000000..64f7db5 --- /dev/null +++ b/Helpers/PullTimerHelper.cs @@ -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 . +*/ + +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( + 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? _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((IntPtr)_agentData + 0x38); + if (countdownActive == 0) + { + _lastMaxValueSet = false; + return; + } + + float countDownPointerValue = Marshal.PtrToStructure((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; + } +} diff --git a/Helpers/SmoothHPHelper.cs b/Helpers/SmoothHPHelper.cs new file mode 100644 index 0000000..3621e56 --- /dev/null +++ b/Helpers/SmoothHPHelper.cs @@ -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; + } + } +} diff --git a/Helpers/SpellHelper.cs b/Helpers/SpellHelper.cs new file mode 100644 index 0000000..e8b0438 --- /dev/null +++ b/Helpers/SpellHelper.cs @@ -0,0 +1,76 @@ +using FFXIVClientStructs.FFXIV.Client.Game; +using System; + +namespace HSUI.Helpers +{ + internal class SpellHelper + { + #region Singleton + private static Lazy _lazyInstance = new Lazy(() => 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(() => 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); + } +} diff --git a/Helpers/TextTagsHelper.cs b/Helpers/TextTagsHelper.cs new file mode 100644 index 0000000..88f9f3f --- /dev/null +++ b/Helpers/TextTagsHelper.cs @@ -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> TextTags = new Dictionary>() + { + #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> ExpTags = new Dictionary>() + { + #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> HealthTextTags = new Dictionary>() + { + #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> ManaTextTags = new Dictionary>() + { + #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> CharaTextTags = new Dictionary>() + { + #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> TitleTextTags = new Dictionary>() + { + #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>> NumericValuesTagMaps = new List>>() + { + 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? func) && func != null) + { + return func(actor, name, length, isPlayerName); + } + + if (ExpTags.TryGetValue(tag, out Func? expFunc) && expFunc != null) + { + return expFunc(actor, name); + } + + if (actor is ICharacter chara && + CharaTextTags.TryGetValue(tag, out Func? charaFunc) && charaFunc != null) + { + return charaFunc(chara); + } + else if (current.HasValue && max.HasValue) + { + foreach (var map in NumericValuesTagMaps) + { + if (map.TryGetValue(tag, out Func? numericFunc) && numericFunc != null) + { + return numericFunc(current.Value, max.Value); + } + } + } + + if (title != null && + TitleTextTags.TryGetValue(tag, out Func? 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 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 ParseGroups(string text) + { + MatchCollection matches = Regex.Matches(text, @"\{(.*?)\}"); + if (matches.Count == 0) + { + return new List() { text }; + } + + List result = new List(); + 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() : "-"; + } + } +} \ No newline at end of file diff --git a/Helpers/TexturesHelper.cs b/Helpers/TexturesHelper.cs new file mode 100644 index 0000000..112f951 --- /dev/null +++ b/Helpers/TexturesHelper.cs @@ -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(uint rowId, uint stackCount = 0, bool hdIcon = true) where T : struct, IExcelRow + { + ExcelSheet sheet = Plugin.DataManager.GetExcelSheet(); + return sheet == null ? null : GetTexture(sheet.GetRow(rowId), stackCount, hdIcon); + } + + public static IDalamudTextureWrap? GetTexture(dynamic row, uint stackCount = 0, bool hdIcon = true) where T : struct, IExcelRow + { + 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(); + } + } +} diff --git a/Helpers/TooltipsHelper.cs b/Helpers/TooltipsHelper.cs new file mode 100644 index 0000000..c71ae0c --- /dev/null +++ b/Helpers/TooltipsHelper.cs @@ -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(); + + 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; + } + } +} diff --git a/Helpers/Utils.cs b/Helpers/Utils.cs new file mode 100644 index 0000000..0280a26 --- /dev/null +++ b/Helpers/Utils.cs @@ -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 StatusListForActor(IGameObject? obj) + { + if (obj is IBattleChara chara) + { + return StatusListForBattleChara(chara); + } + + return new List(); + } + + public static IEnumerable StatusListForBattleChara(IBattleChara? chara) + { + List statusList = new List(); + 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; + } + + /// + /// Gets the actual target ID of your targets target. + /// + /// Your target + /// Target ID of your targets targer. Returns -1 if old code should be ran. + 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"); + } + } +} diff --git a/Helpers/WhosTalkingHelper.cs b/Helpers/WhosTalkingHelper.cs new file mode 100644 index 0000000..a224570 --- /dev/null +++ b/Helpers/WhosTalkingHelper.cs @@ -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 _getUserState; + private Dictionary _cachedStates = new Dictionary(); + + private string speakingPath = ""; + private string mutedPath = ""; + private string deafenedPath = ""; + + #region Singleton + private WhosTalkingHelper() + { + _getUserState = Plugin.PluginInterface.GetIpcSubscriber("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; + } + } +} diff --git a/Helpers/WotsitHelper.cs b/Helpers/WotsitHelper.cs new file mode 100644 index 0000000..ea258a9 --- /dev/null +++ b/Helpers/WotsitHelper.cs @@ -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 _registerWithSearch; + private readonly ICallGateSubscriber _invoke; + private readonly ICallGateSubscriber _unregisterAll; + + private Dictionary _map = new Dictionary(); + + #region Singleton + private WotsitHelper() + { + _registerWithSearch = Plugin.PluginInterface.GetIpcSubscriber("FA.RegisterWithSearch"); + _unregisterAll = Plugin.PluginInterface.GetIpcSubscriber("FA.UnregisterAll"); + + _invoke = Plugin.PluginInterface.GetIpcSubscriber("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; + } + } + } +} diff --git a/Interface/Bars/BarConfig.cs b/Interface/Bars/BarConfig.cs new file mode 100644 index 0000000..7ae3da7 --- /dev/null +++ b/Interface/Bars/BarConfig.cs @@ -0,0 +1,79 @@ +using HSUI.Config; +using HSUI.Config.Attributes; +using System.Numerics; +using HSUI.Interface.GeneralElements; +using HSUI.Enums; + +namespace HSUI.Interface.Bars +{ + [Exportable(false)] + public class BarConfig : AnchorablePluginConfigObject + { + [ColorEdit4("Background Color")] + [Order(16)] + public PluginConfigColor BackgroundColor = new PluginConfigColor(new Vector4(0f / 255f, 0f / 255f, 0f / 255f, 50f / 100f)); + + [ColorEdit4("Fill Color")] + [Order(25)] + public PluginConfigColor FillColor; + + [Combo("Fill Direction", new string[] { "Left", "Right", "Up", "Down" })] + [Order(30)] + public BarDirection FillDirection; + + [BarTexture("Bar Texture", spacing = true, help = "Default means the bar will be drawn using the global gradient configuration for bars found in Colors > Misc.")] + [Order(31)] + public string BarTextureName = ""; + + [BarTextureDrawMode("Draw Mode")] + [Order(32)] + public BarTextureDrawMode BarTextureDrawMode = BarTextureDrawMode.Stretch; + + [Checkbox("Show Border", spacing = true)] + [Order(35)] + public bool DrawBorder = true; + + [ColorEdit4("Border Color")] + [Order(36, collapseWith = nameof(DrawBorder))] + public PluginConfigColor BorderColor = new PluginConfigColor(new Vector4(0f / 255f, 0f / 255f, 0f / 255f, 100f / 100f)); + + [DragInt("Border Thickness", min = 1, max = 10)] + [Order(37, collapseWith = nameof(DrawBorder))] + public int BorderThickness = 1; + + [NestedConfig("Shadow", 40, spacing = true)] + public ShadowConfig ShadowConfig = new ShadowConfig() { Enabled = false }; + + [Checkbox("Hide When Inactive", spacing = true)] + [Order(41)] + public bool HideWhenInactive = false; + + public BarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor, BarDirection fillDirection = BarDirection.Right) + { + Position = position; + Size = size; + FillColor = fillColor; + FillDirection = fillDirection; + } + } + + [Exportable(false)] + public class BarGlowConfig : PluginConfigObject + { + [ColorEdit4("Color")] + [Order(5)] + public PluginConfigColor Color = new PluginConfigColor(new Vector4(255f / 255f, 255f / 255f, 255f / 255f, 50f / 100f)); + + [DragInt("Size", min = 1, max = 100)] + [Order(25)] + public int Size = 1; + } + + public enum BarDirection + { + Left, + Right, + Up, + Down + } +} diff --git a/Interface/Bars/BarHud.cs b/Interface/Bars/BarHud.cs new file mode 100644 index 0000000..fdb09fe --- /dev/null +++ b/Interface/Bars/BarHud.cs @@ -0,0 +1,210 @@ +using Dalamud.Game.ClientState.Objects.Types; +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.Numerics; + +namespace HSUI.Interface.Bars +{ + public class BarHud + { + private string ID { get; set; } + + private Rect BackgroundRect { get; set; } = new Rect(); + + private List ForegroundRects { get; set; } = new List(); + + private List LabelHuds { get; set; } = new List(); + + private bool DrawBorder { get; set; } + + private PluginConfigColor? BorderColor { get; set; } + + private int BorderThickness { get; set; } + + private DrawAnchor Anchor { get; set; } + + private IGameObject? Actor { get; set; } + + private PluginConfigColor? GlowColor { get; set; } + + private int GlowSize { get; set; } + + private float? Current; + private float? Max; + + private ShadowConfig? ShadowConfig { get; set; } + + private string? BarTextureName { get; set; } + private BarTextureDrawMode BarTextureDrawMode { get; set; } + + public bool NeedsInputs = false; + + public BarHud( + string id, + bool drawBorder = true, + PluginConfigColor? borderColor = null, + int borderThickness = 1, + DrawAnchor anchor = DrawAnchor.TopLeft, + IGameObject? actor = null, + PluginConfigColor? glowColor = null, + int? glowSize = 1, + float? current = null, + float? max = null, + ShadowConfig? shadowConfig = null, + string? barTextureName = null, + BarTextureDrawMode barTextureDrawMode = BarTextureDrawMode.Stretch) + { + ID = id; + DrawBorder = drawBorder; + BorderColor = borderColor; + BorderThickness = borderThickness; + Anchor = anchor; + Actor = actor; + GlowColor = glowColor; + GlowSize = glowSize ?? 1; + Current = current; + Max = max; + ShadowConfig = shadowConfig; + BarTextureName = barTextureName; + BarTextureDrawMode = barTextureDrawMode; + } + + public BarHud(BarConfig config, IGameObject? actor = null, BarGlowConfig? glowConfig = null, float? current = null, float? max = null) + : this(config.ID, + config.DrawBorder, + config.BorderColor, + config.BorderThickness, + config.Anchor, + actor, + glowConfig?.Color, + glowConfig?.Size, + current, + max, + null, + config.BarTextureName, + config.BarTextureDrawMode) + { + BackgroundRect = new Rect(config.Position, config.Size, config.BackgroundColor); + ShadowConfig = config.ShadowConfig; + } + + public BarHud SetBackground(Rect rect) + { + BackgroundRect = rect; + return this; + } + + public BarHud AddForegrounds(params Rect[] rects) + { + ForegroundRects.AddRange(rects); + return this; + } + + public BarHud AddLabels(params LabelConfig[]? labels) + { + if (labels != null) + { + foreach (LabelConfig config in labels) + { + var labelHud = new LabelHud(config); + LabelHuds.Add(labelHud); + } + } + + return this; + } + + public BarHud SetGlow(PluginConfigColor color, int size = 1) + { + GlowColor = color; + GlowSize = size; + + return this; + } + + public void Draw(Vector2 origin) + { + var barPos = Utils.GetAnchoredPosition(origin, BackgroundRect.Size, Anchor); + var backgroundPos = barPos + BackgroundRect.Position; + + DrawRects(barPos, backgroundPos); + + // labels + foreach (LabelHud label in LabelHuds) + { + label.Draw(backgroundPos, BackgroundRect.Size, Actor, null, (uint?)Current, (uint?)Max); + } + } + + public List<(StrataLevel, Action)> GetDrawActions(Vector2 origin, StrataLevel strataLevel) + { + List<(StrataLevel, Action)> drawActions = new List<(StrataLevel, Action)>(); + + var barPos = Utils.GetAnchoredPosition(origin, BackgroundRect.Size, Anchor); + var backgroundPos = barPos + BackgroundRect.Position; + + drawActions.Add((strataLevel, () => + { + DrawRects(barPos, backgroundPos); + } + )); + + // labels + foreach (LabelHud label in LabelHuds) + { + drawActions.Add((label.GetConfig().StrataLevel, () => + { + label.Draw(backgroundPos, BackgroundRect.Size, Actor, null, (uint?)Current, (uint?)Max); + } + )); + } + + return drawActions; + } + + private void DrawRects(Vector2 barPos, Vector2 backgroundPos) + { + DrawHelper.DrawInWindow(ID, backgroundPos, BackgroundRect.Size, NeedsInputs, (drawList) => + { + // Draw background + drawList.AddRectFilled(backgroundPos, backgroundPos + BackgroundRect.Size, BackgroundRect.Color.Base); + + // Draw Shadow + if (ShadowConfig != null && ShadowConfig.Enabled) + { + // Right Side + drawList.AddRectFilled(backgroundPos + new Vector2(BackgroundRect.Size.X, ShadowConfig.Offset), backgroundPos + BackgroundRect.Size + new Vector2(ShadowConfig.Offset, ShadowConfig.Offset) + new Vector2(ShadowConfig.Thickness - 1, ShadowConfig.Thickness - 1), ShadowConfig.Color.Base); + + // Bottom Size + drawList.AddRectFilled(backgroundPos + new Vector2(ShadowConfig.Offset, BackgroundRect.Size.Y), backgroundPos + BackgroundRect.Size + new Vector2(ShadowConfig.Offset, ShadowConfig.Offset) + new Vector2(ShadowConfig.Thickness - 1, ShadowConfig.Thickness - 1), ShadowConfig.Color.Base); + } + + // Draw foregrounds + foreach (Rect rect in ForegroundRects) + { + DrawHelper.DrawBarTexture(barPos + rect.Position, rect.Size, rect.Color, BarTextureName, BarTextureDrawMode, drawList); + } + + // Draw Border + if (DrawBorder) + { + drawList.AddRect(backgroundPos, backgroundPos + BackgroundRect.Size, BorderColor?.Base ?? 0xFF000000, 0, ImDrawFlags.None, BorderThickness); + } + + // Draw Glow + if (GlowColor != null) + { + var glowPosition = new Vector2(backgroundPos.X - 1, backgroundPos.Y - 1); + var glowSize = new Vector2(BackgroundRect.Size.X + 2, BackgroundRect.Size.Y + 2); + + drawList.AddRect(glowPosition, glowPosition + glowSize, GlowColor.Base, 0, ImDrawFlags.None, GlowSize); + } + }); + } + } +} diff --git a/Interface/Bars/BarUtilities.cs b/Interface/Bars/BarUtilities.cs new file mode 100644 index 0000000..f10afb1 --- /dev/null +++ b/Interface/Bars/BarUtilities.cs @@ -0,0 +1,441 @@ +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Game.ClientState.Statuses; +using HSUI.Config; +using HSUI.Enums; +using HSUI.Helpers; +using HSUI.Interface.GeneralElements; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace HSUI.Interface.Bars +{ + public class BarUtilities + { + public static BarHud GetProgressBar(ProgressBarConfig config, float current, float max, float min = 0f, IGameObject? actor = null, PluginConfigColor? fillColor = null, BarGlowConfig? barGlowConfig = null) + { + return GetProgressBar(config, config.ThresholdConfig, new LabelConfig[] { config.Label }, current, max, min, actor, fillColor, barGlowConfig); + } + + public static BarHud GetProgressBar( + BarConfig config, + ThresholdConfig? thresholdConfig, + LabelConfig[]? labelConfigs, + float current, + float max, + float min = 0f, + IGameObject? actor = null, + PluginConfigColor? fillColor = null, + BarGlowConfig? glowConfig = null, + PluginConfigColor? backgroundColor = null + ) + { + BarHud bar = new(config, actor, glowConfig, current, max); + + PluginConfigColor color = fillColor ?? config.FillColor; + if (thresholdConfig != null) + { + color = thresholdConfig.ChangeColor && thresholdConfig.IsActive(current) ? thresholdConfig.Color : color; + } + + Rect foreground = GetFillRect(config.Position, config.Size, config.FillDirection, color, current, max, min); + bar.AddForegrounds(foreground); + bar.AddLabels(labelConfigs); + + if (backgroundColor != null) + { + Rect bg = new Rect(config.Position, config.Size, backgroundColor); + bar.SetBackground(bg); + } + + AddThresholdMarker(bar, config, thresholdConfig, max, min); + + return bar; + } + + public static BarHud? GetProcBar( + ProgressBarConfig config, + IPlayerCharacter player, + uint statusId, + float maxDuration, + bool trackDuration = true) + { + return GetProcBar(config, player, new List { statusId }, new List { maxDuration }, trackDuration); + } + + public static BarHud? GetProcBar( + ProgressBarConfig config, + IPlayerCharacter player, + List statusIDs, + List maxDurations, + bool trackDuration = true) + { + if (statusIDs.Count == 0 || maxDurations.Count == 0) { return null; } + + IStatus? status = Utils.StatusListForBattleChara(player).FirstOrDefault(o => statusIDs.Contains(o.StatusId)); + if (status == null && config.HideWhenInactive) + { + return null; + } + + float duration = Math.Abs(status?.RemainingTime ?? 0); + + if (trackDuration) + { + int index = status != null ? statusIDs.IndexOf(status.StatusId) : 0; + config.Label.SetValue(duration); + return GetProgressBar(config, duration, maxDurations[index], 0, player); + } + + config.Label.SetText(""); + return GetBar(config, duration <= 0 ? 0 : 1, 1, 0); + } + + public static BarHud? GetDoTBar( + ProgressBarConfig config, + IPlayerCharacter player, + IGameObject? target, + uint statusId, + float maxDuration) + { + return GetDoTBar(config, player, target, new List { statusId }, new List { maxDuration }); + } + + public static BarHud? GetDoTBar( + ProgressBarConfig config, + IPlayerCharacter player, + IGameObject? target, + List statusIDs, + List maxDurations) + { + if (statusIDs.Count == 0 || maxDurations.Count == 0) { return null; } + + IStatus? status = null; + + if (target != null && target is IBattleChara targetChara) + { + status = Utils.StatusListForBattleChara(targetChara).FirstOrDefault(o => o.SourceId == player.GameObjectId && statusIDs.Contains(o.StatusId)); + } + + if (status == null && config.HideWhenInactive) + { + return null; + } + + int index = status != null ? statusIDs.IndexOf(status.StatusId) : 0; + float duration = Math.Abs(status?.RemainingTime ?? 0); + float maxDuration = maxDurations[index]; + + config.Label.SetValue(duration); + return GetProgressBar(config, duration, maxDuration, 0, player); + } + + private static void AddThresholdMarker(BarHud bar, BarConfig config, ThresholdConfig? thresholdConfig, float max, float min) + { + if (thresholdConfig == null || !thresholdConfig.Enabled || !thresholdConfig.ShowMarker) + { + return; + } + + float thresholdPercent = Math.Clamp(thresholdConfig.Value / (max - min), 0f, 1f); + Vector2 offset = GetFillDirectionOffset( + new Vector2(config.Size.X * thresholdPercent, config.Size.Y * thresholdPercent), + config.FillDirection + ); + + Vector2 markerSize = config.FillDirection.IsHorizontal() ? + new Vector2(thresholdConfig.MarkerSize, config.Size.Y) : + new Vector2(config.Size.X, thresholdConfig.MarkerSize); + + Vector2 markerPos = config.FillDirection.IsInverted() ? + config.Position + GetFillDirectionOffset(config.Size, config.FillDirection) - offset : + config.Position + offset; + + Vector2 anchoredPos = Utils.GetAnchoredPosition(markerPos, markerSize, config.FillDirection.IsHorizontal() ? DrawAnchor.Top : DrawAnchor.Left); + Rect marker = new(anchoredPos, markerSize, thresholdConfig.MarkerColor); + bar.AddForegrounds(marker); + } + + // Tuple is + public static BarHud[] GetChunkedBars( + ChunkedBarConfig config, + Tuple[] chunks, + IGameObject? actor, + BarGlowConfig glowConfig) + { + List chunksToGlowList = new(); + for (int i = 0; i < chunks.Length; i++) + { + chunksToGlowList.Add(chunks[i].Item2 >= 1f); + } + + return GetChunkedBars(config, chunks, actor, glowConfig, chunksToGlowList.ToArray()); + } + + public static BarHud[] GetChunkedBars( + ChunkedBarConfig config, + Tuple[] chunks, + IGameObject? actor, + BarGlowConfig? glowConfig = null, + bool[]? chunksToGlow = null) + { + BarHud[] bars = new BarHud[chunks.Length]; + Vector2 pos = Utils.GetAnchoredPosition(config.Position, config.Size, config.Anchor); + + for (int i = 0; i < chunks.Length; i++) + { + Vector2 chunkPos, chunkSize; + if (config.FillDirection.IsHorizontal()) + { + chunkSize = new Vector2((config.Size.X - config.Padding * (chunks.Length - 1)) / chunks.Length, config.Size.Y); + chunkPos = pos + new Vector2((chunkSize.X + config.Padding) * i, 0); + } + else + { + chunkSize = new Vector2(config.Size.X, (config.Size.Y - config.Padding * (chunks.Length - 1)) / chunks.Length); + chunkPos = pos + new Vector2(0, (chunkSize.Y + config.Padding) * i); + } + + Rect background = new(chunkPos, chunkSize, config.BackgroundColor); + Rect foreground = GetFillRect(chunkPos, chunkSize, config.FillDirection, chunks[i].Item1, chunks[i].Item2, 1f, 0f); + BarGlowConfig? glow = (glowConfig?.Enabled == true && chunksToGlow?[i] == true) ? glowConfig : null; + + bars[i] = new BarHud(config.ID + i, + config.DrawBorder, + config.BorderColor, + config.BorderThickness, + actor: actor, + glowColor: glow?.Color, + glowSize: glow?.Size, + barTextureName: config.BarTextureName, + barTextureDrawMode: config.BarTextureDrawMode, + shadowConfig: config.ShadowConfig + ); + bars[i].SetBackground(background); + bars[i].AddForegrounds(foreground); + + LabelConfig? label = chunks[i].Item3; + if (label is not null) + { + bars[i].AddLabels(label); + } + } + + return bars; + } + + public static BarHud[] GetChunkedBars( + ChunkedBarConfig config, + int chunks, + float current, + float max, + float min = 0f, + IGameObject? actor = null, + LabelConfig?[]? labels = null, + PluginConfigColor? fillColor = null, + PluginConfigColor? partialFillColor = null, + BarGlowConfig? glowConfig = null, + bool[]? chunksToGlow = null) + { + float chunkRange = (max - min) / chunks; + + var barChunks = new Tuple[chunks]; + for (int i = 0; i < chunks; i++) + { + int barIndex = config.FillDirection.IsInverted() ? chunks - i - 1 : i; + float chunkMin = min + chunkRange * i; + float chunkMax = min + chunkRange * (i + 1); + float chunkPercent = Math.Clamp((current - chunkMin) / (chunkMax - chunkMin), 0f, 1f); + + PluginConfigColor chunkColor = partialFillColor != null && current < chunkMax ? partialFillColor : fillColor ?? config.FillColor; + barChunks[barIndex] = new Tuple(chunkColor, chunkPercent, labels?[i]); + } + + if (glowConfig != null && chunksToGlow == null) + { + return GetChunkedBars(config, barChunks, actor, glowConfig); + } + + return GetChunkedBars(config, barChunks, actor, glowConfig, chunksToGlow); + } + + public static BarHud[] GetChunkedProgressBars( + ChunkedProgressBarConfig config, + int chunks, + float current, + float max, + float min = 0f, + IGameObject? actor = null, + BarGlowConfig? glowConfig = null, + PluginConfigColor? fillColor = null, + int thresholdChunk = 1, + bool[]? chunksToGlow = null, + int forceLabelIndex = -1) + { + var color = fillColor ?? config.FillColor; + + if (config.UseChunks) + { + NumericLabelConfig?[] labels = new NumericLabelConfig?[chunks]; + for (int i = 0; i < chunks; i++) + { + float chunkRange = (max - min) / chunks; + float chunkMin = min + chunkRange * i; + float chunkMax = min + chunkRange * (i + 1); + float chunkPercent = Math.Clamp((current - chunkMin) / (chunkMax - chunkMin), 0f, 1f); + + NumericLabelConfig? label = config.Label; + if (forceLabelIndex == -1) + { + switch (config.LabelMode) + { + case LabelMode.AllChunks: + label = config.Label.Clone(i); + label.SetValue(Math.Clamp(current - chunkMin, 0, chunkRange)); + break; + case LabelMode.ActiveChunk: + label = chunkPercent < 1f && chunkPercent > 0f ? config.Label.Clone(i) : null; + break; + }; + } + else + { + label = forceLabelIndex == i ? config.Label : null; + } + + labels[i] = label; + } + + var partialColor = config.UsePartialFillColor ? config.PartialFillColor : null; + return GetChunkedBars(config, chunks, current, max, min, actor, labels, color, partialColor, glowConfig, chunksToGlow); + } + + var threshold = GetThresholdConfigForChunk(config, thresholdChunk, chunks, min, max); + BarHud bar = GetProgressBar(config, threshold, new LabelConfig[] { config.Label }, current, max, min, actor, color, glowConfig); + return new BarHud[] { bar }; + } + + public static Rect[] GetShieldForeground( + ShieldConfig shieldConfig, + Vector2 pos, + Vector2 size, + Vector2 healthFillSize, + BarDirection fillDirection, + float shieldPercent, + float currentHp, + float maxHp, + PluginConfigColor? color = null) + { + float shieldValue = shieldPercent * maxHp; + float overshield = shieldConfig.FillHealthFirst ? Math.Max(shieldValue + currentHp - maxHp, 0f) : shieldValue; + float shieldSize = shieldConfig.Height; + PluginConfigColor c = color ?? shieldConfig.Color; + + if (!shieldConfig.HeightInPixels) + { + shieldSize = (fillDirection.IsHorizontal() ? size.Y : size.X) * shieldConfig.Height / 100f; + } + + var overshieldSize = fillDirection.IsHorizontal() + ? new Vector2(size.X, Math.Min(shieldSize, size.Y)) + : new Vector2(Math.Min(shieldSize, size.X), size.Y); + + Rect overshieldFill = GetFillRect(pos, overshieldSize, fillDirection, c, overshield, maxHp); + + if (shieldConfig.FillHealthFirst && currentHp < maxHp) + { + var shieldPos = fillDirection.IsInverted() ? pos : pos + GetFillDirectionOffset(healthFillSize, fillDirection); + var shieldFillSize = size - GetFillDirectionOffset(healthFillSize, fillDirection); + var healthFillShieldSize = fillDirection.IsHorizontal() + ? new Vector2(shieldFillSize.X, Math.Min(shieldSize, size.Y)) + : new Vector2(Math.Min(shieldSize, size.X), shieldFillSize.Y); + + Rect shieldFill = GetFillRect(shieldPos, healthFillShieldSize, fillDirection, c, shieldValue - overshield, maxHp - currentHp, 0f); + return new[] { overshieldFill, shieldFill }; + } + + return new[] { overshieldFill }; + } + + public static BarHud GetBar( + BarConfig Config, + float current, + float max, + float min = 0f, + IGameObject? actor = null, + PluginConfigColor? fillColor = null, + BarGlowConfig? glowConfig = null, + LabelConfig[]? labels = null) + { + Rect foreground = GetFillRect(Config.Position, Config.Size, Config.FillDirection, fillColor ?? Config.FillColor, current, max, min); + + BarHud bar = new BarHud(Config, actor, glowConfig); + bar.AddForegrounds(foreground); + bar.AddLabels(labels); + + return bar; + } + + /// + /// Gets the horizonal or vertical offset depending on the fill direction. + /// + public static Vector2 GetFillDirectionOffset(Vector2 size, BarDirection fillDirection) + { + return fillDirection.IsHorizontal() ? new(size.X, 0) : new(0, size.Y); + } + + public static Rect GetFillRect(Vector2 pos, Vector2 size, BarDirection fillDirection, PluginConfigColor color, float current, float max, float min = 0f) + { + float fillPercent = max == 0 ? 1f : Math.Clamp((current - min) / (max - min), 0f, 1f); + + Vector2 fillPos = Vector2.Zero; + Vector2 fillSize = fillDirection.IsHorizontal() ? new(size.X * fillPercent, size.Y) : new(size.X, size.Y * fillPercent); + if (fillDirection == BarDirection.Left) + { + fillPos = Utils.GetAnchoredPosition(new(size.X, 0), fillSize, DrawAnchor.TopRight); + } + else if (fillDirection == BarDirection.Up) + { + fillPos = Utils.GetAnchoredPosition(new(0, size.Y), fillSize, DrawAnchor.BottomLeft); + } + + return new Rect(pos + fillPos, fillSize, color); + } + + public static ThresholdConfig GetThresholdConfigForChunk(ChunkedProgressBarConfig config, int chunk, int chunks, float min, float max) => + new ThresholdConfig + { + ThresholdType = ThresholdType.Below, + Color = config.PartialFillColor, + Enabled = config.UsePartialFillColor, + Value = (max - min) / chunks * chunk, + ChangeColor = true, + ShowMarker = false + }; + + public static void AddShield(BarHud bar, BarConfig config, ShieldConfig shieldConfig, ICharacter character, Vector2 fillSize, PluginConfigColor? color = null) + { + if (shieldConfig.Enabled) + { + float shield = Utils.ActorShieldValue(character); + if (shield > 0f) + { + bar.AddForegrounds( + GetShieldForeground( + shieldConfig, + config.Position, + config.Size, + fillSize, + config.FillDirection, + shield, + character.CurrentHp, + character.MaxHp, + color) + ); + } + } + } + } +} diff --git a/Interface/Bars/ChunkedBarConfig.cs b/Interface/Bars/ChunkedBarConfig.cs new file mode 100644 index 0000000..66d5f73 --- /dev/null +++ b/Interface/Bars/ChunkedBarConfig.cs @@ -0,0 +1,83 @@ +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Enums; +using HSUI.Interface.GeneralElements; +using System.Numerics; + +namespace HSUI.Interface.Bars +{ + [Exportable(false)] + public class ChunkedBarConfig : BarConfig + { + [DragInt("Padding", min = -4000, max = 4000)] + [Order(45)] + public int Padding = 2; + + public ChunkedBarConfig( + Vector2 position, + Vector2 size, + PluginConfigColor fillColor, + int padding = 2) : base(position, size, fillColor) + { + Padding = padding; + } + } + + [Exportable(false)] + public class ChunkedProgressBarConfig : ChunkedBarConfig + { + [Checkbox("Show In Chunks", spacing = true)] + [Order(46)] + public bool UseChunks = true; + + [RadioSelector("Show Text on All Chunks", "Show Text on Active Chunk")] + [Order(47, collapseWith = nameof(UseChunks))] + public LabelMode LabelMode; + + [Checkbox("Use Partial Fill Color", spacing = true)] + [Order(50)] + public bool UsePartialFillColor = false; + + [ColorEdit4("Partial Fill Color")] + [Order(55, collapseWith = nameof(UsePartialFillColor))] + public PluginConfigColor PartialFillColor; + + [NestedConfig("Bar Text", 1000, separator = false, spacing = true)] + public NumericLabelConfig Label; + + public ChunkedProgressBarConfig( + Vector2 position, + Vector2 size, + PluginConfigColor fillColor, + int padding = 2, + PluginConfigColor? partialFillColor = null) : base(position, size, fillColor, padding) + { + Label = new NumericLabelConfig(Vector2.Zero, "", DrawAnchor.Center, DrawAnchor.Center); + Label.Enabled = false; + + PartialFillColor = partialFillColor ?? new PluginConfigColor(new(180f / 255f, 180f / 255f, 180f / 255f, 100f / 100f)); + } + } + + [DisableParentSettings("LabelMode", "UsePartialFillColor", "PartialFillColor")] + [Exportable(false)] + public class StacksWithDurationBarConfig : ChunkedProgressBarConfig + { + public StacksWithDurationBarConfig( + Vector2 position, + Vector2 size, + PluginConfigColor fillColor, + int padding = 2, + PluginConfigColor? partialFillColor = null) : base(position, size, fillColor, padding) + { + UseChunks = true; + UsePartialFillColor = false; + } + } + + public enum LabelMode + { + AllChunks, + ActiveChunk + } +} diff --git a/Interface/Bars/ProgressBarConfig.cs b/Interface/Bars/ProgressBarConfig.cs new file mode 100644 index 0000000..feb41bb --- /dev/null +++ b/Interface/Bars/ProgressBarConfig.cs @@ -0,0 +1,80 @@ +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Enums; +using HSUI.Interface.GeneralElements; +using System.Numerics; + +namespace HSUI.Interface.Bars +{ + [Exportable(false)] + public class ProgressBarConfig : BarConfig + { + [NestedConfig("Threshold", 45)] + public ThresholdConfig ThresholdConfig = new ThresholdConfig(); + + [NestedConfig("Bar Text", 1000)] + public NumericLabelConfig Label; + + public ProgressBarConfig( + Vector2 position, + Vector2 size, + PluginConfigColor fillColor, + BarDirection fillDirection = BarDirection.Right, + PluginConfigColor? threshHoldColor = null, + float threshold = 0f) : base(position, size, fillColor, fillDirection) + { + Label = new NumericLabelConfig(Vector2.Zero, "", DrawAnchor.Center, DrawAnchor.Center); + ThresholdConfig.Color = threshHoldColor ?? ThresholdConfig.Color; + ThresholdConfig.Value = threshold; + } + } + + [Exportable(false)] + public class ThresholdConfig : PluginConfigObject + { + [DragFloat("Threshold Value", min = 0f, max = 10000f)] + [Order(10)] + public float Value = 0f; + + [Checkbox("Change Color")] + [Order(15)] + public bool ChangeColor = true; + + [Combo("Activate Above/Below Threshold", "Above", "Below")] + [Order(20, collapseWith = nameof(ChangeColor))] + public ThresholdType ThresholdType = ThresholdType.Below; + + [ColorEdit4("Color")] + [Order(25, collapseWith = nameof(ChangeColor))] + public PluginConfigColor Color = new PluginConfigColor(new(230f / 255f, 33f / 255f, 33f / 255f, 100f / 100f)); + + [Checkbox("Show Threshold Marker")] + [Order(30)] + public bool ShowMarker = false; + + [DragInt("Threshold Marker Size", min = 0, max = 10000)] + [Order(35, collapseWith = nameof(ShowMarker))] + public int MarkerSize = 2; + + [ColorEdit4("Threshold Marker Color")] + [Order(40, collapseWith = nameof(ShowMarker))] + public PluginConfigColor MarkerColor = new PluginConfigColor(new Vector4(0f / 255f, 0f / 255f, 0f / 255f, 100f / 100f)); + + public bool IsActive(float current) + { + return Enabled && (ThresholdType == ThresholdType.Below && current < Value || + ThresholdType == ThresholdType.Above && current > Value); + } + + public ThresholdConfig() + { + Enabled = false; + } + } + + public enum ThresholdType + { + Above, + Below + } +} diff --git a/Interface/Bars/Rect.cs b/Interface/Bars/Rect.cs new file mode 100644 index 0000000..ada2af2 --- /dev/null +++ b/Interface/Bars/Rect.cs @@ -0,0 +1,23 @@ +using HSUI.Config; +using System.Numerics; + +namespace HSUI.Interface.Bars +{ + public class Rect + { + public Vector2 Position { get; set; } + + public Vector2 Size { get; set; } + + public PluginConfigColor Color { get; set; } + + public Rect(Vector2 pos, Vector2 size, PluginConfigColor? color = null) + { + Position = pos; + Size = size; + Color = color ?? new PluginConfigColor(new(0, 0, 0, 0)); + } + + public Rect() : this(new(0, 0), new(0, 0), new PluginConfigColor(new(0, 0, 0, 0))) { } + } +} diff --git a/Interface/DraggableHudElement.cs b/Interface/DraggableHudElement.cs new file mode 100644 index 0000000..1178b96 --- /dev/null +++ b/Interface/DraggableHudElement.cs @@ -0,0 +1,302 @@ +using Dalamud.Logging; +using HSUI.Config; +using HSUI.Enums; +using HSUI.Helpers; +using Dalamud.Bindings.ImGui; +using System; +using System.Collections.Generic; +using System.Numerics; + +namespace HSUI.Interface +{ + public delegate void DraggableHudElementSelectHandler(DraggableHudElement element); + + public class DraggableHudElement : HudElement + { + public DraggableHudElement(MovablePluginConfigObject config, string? displayName = null) : base(config) + { + _displayName = displayName ?? ID; + } + + public event DraggableHudElementSelectHandler? SelectEvent; + public bool Selected = false; + + private string _displayName; + protected bool _windowPositionSet = false; + private Vector2 _lastWindowPos = Vector2.Zero; + private Vector2 _positionOffset; + private Vector2 _contentMargin = new Vector2(4, 0); + + private bool _draggingEnabled = false; + public bool DraggingEnabled + { + get => _draggingEnabled; + set + { + _draggingEnabled = value; + + if (_draggingEnabled) + { + _windowPositionSet = false; + _minPos = null; + _maxPos = null; + } + } + } + + public bool CanTakeInputForDrag = false; + public bool NeedsInputForDrag { get; private set; } = false; + + public virtual Vector2 ParentPos() { return Vector2.Zero; } // override + + protected sealed override void CreateDrawActions(Vector2 origin) + { + if (_draggingEnabled) + { + AddDrawAction(_config.StrataLevel, () => + { + DrawDraggableArea(origin); + }); + return; + } + + DrawChildren(origin); + } + + public virtual void DrawChildren(Vector2 origin) { } + + private bool CalculateNeedsInput(Vector2 pos, Vector2 size, bool selected) + { + Vector2 mousePos = ImGui.GetMousePos(); + + if (ImGui.IsMouseHoveringRect(pos, pos + size)) + { + return true; + } + + if (!selected) + { + return false; + } + + var arrowsPos = DraggablesHelper.GetArrowPositions(pos, size); + + foreach (Vector2 arrowPos in arrowsPos) + { + if (ImGui.IsMouseHoveringRect(arrowPos, arrowPos + DraggablesHelper.ArrowSize)) + { + return true; + } + } + + return false; + } + + protected virtual void DrawDraggableArea(Vector2 origin) + { + var windowFlags = ImGuiWindowFlags.NoScrollbar + | ImGuiWindowFlags.NoTitleBar + | ImGuiWindowFlags.NoResize + | ImGuiWindowFlags.NoBackground + | ImGuiWindowFlags.NoDecoration + | ImGuiWindowFlags.NoSavedSettings; + + // always update size + var size = MaxPos - MinPos + _contentMargin * 2; + ImGui.SetNextWindowSize(size, ImGuiCond.Always); + + // needs input? + NeedsInputForDrag = CanTakeInputForDrag && CalculateNeedsInput(_lastWindowPos, size, Selected); + + if (!NeedsInputForDrag) + { + windowFlags |= ImGuiWindowFlags.NoMove; + } + + // set initial position + if (!_windowPositionSet) + { + ImGui.SetNextWindowPos(origin + MinPos - _contentMargin); + _windowPositionSet = true; + + _positionOffset = _config.Position - MinPos + _contentMargin; + } + + // update config object position + ImGui.Begin(ID + "_dragArea", windowFlags); + var windowPos = ImGui.GetWindowPos(); + _lastWindowPos = windowPos; + _config.Position = windowPos + _positionOffset - origin; + + // check selection + var tooltipText = "x: " + _config.Position.X.ToString() + " y: " + _config.Position.Y.ToString(); + + if (NeedsInputForDrag && ImGui.IsMouseHoveringRect(windowPos, windowPos + size)) + { + bool cliked = ImGui.IsMouseClicked(ImGuiMouseButton.Left) || ImGui.IsMouseDown(ImGuiMouseButton.Left); + if (cliked && !Selected) + { + SelectEvent?.Invoke(this); + } + + // tooltip + TooltipsHelper.Instance.ShowTooltipOnCursor(tooltipText); + } + + // draw window + var drawList = ImGui.GetWindowDrawList(); + var contentPos = windowPos + _contentMargin; + var contentSize = size - _contentMargin * 2; + + // draw draggable indicators + drawList.AddRectFilled(contentPos, contentPos + contentSize, 0x88444444, 3); + + var lineColor = Selected ? 0xEEFFFFFF : 0x66FFFFFF; + drawList.AddRect(contentPos, contentPos + contentSize, lineColor, 3, ImDrawFlags.None, 2); + drawList.AddLine(contentPos + new Vector2(contentSize.X / 2f, 0), contentPos + new Vector2(contentSize.X / 2, contentSize.Y), lineColor); + drawList.AddLine(contentPos + new Vector2(0, contentSize.Y / 2f), contentPos + new Vector2(contentSize.X, contentSize.Y / 2), lineColor); + + ImGui.End(); + + // arrows + if (Selected) + { + if (DraggablesHelper.DrawArrows(windowPos, size, tooltipText, out var movement)) + { + _minPos = null; + _maxPos = null; + _config.Position += movement; + _windowPositionSet = false; + } + } + + // element name + var textSize = ImGui.CalcTextSize(_displayName); + var textColor = Selected ? 0xFFFFFFFF : 0xEEFFFFFF; + var textOutlineColor = Selected ? 0xFF000000 : 0xEE000000; + DrawHelper.DrawOutlinedText(_displayName, contentPos + contentSize / 2f - textSize / 2f, textColor, textOutlineColor, drawList); + } + + #region draggable area + protected Vector2? _minPos = null; + public Vector2 MinPos + { + get + { + if (_minPos != null) + { + return (Vector2)_minPos; + } + + var (positions, sizes) = ChildrenPositionsAndSizes(); + if (positions.Count == 0 || sizes.Count == 0) + { + return Vector2.Zero; + } + + float minX = float.MaxValue; + float minY = float.MaxValue; + + var anchorConfig = _config as AnchorablePluginConfigObject; + for (int i = 0; i < positions.Count; i++) + { + var pos = GetAnchoredPosition(positions[i], sizes[i], anchorConfig?.Anchor ?? DrawAnchor.Center); + minX = Math.Min(minX, pos.X); + minY = Math.Min(minY, pos.Y); + } + + _minPos = new Vector2(minX, minY); + return (Vector2)_minPos; + } + } + + protected Vector2? _maxPos = null; + public Vector2 MaxPos + { + get + { + if (_maxPos != null) + { + return (Vector2)_maxPos; + } + + var (positions, sizes) = ChildrenPositionsAndSizes(); + if (positions.Count == 0 || sizes.Count == 0) + { + return Vector2.Zero; + } + + float maxX = float.MinValue; + float maxY = float.MinValue; + + var anchorConfig = _config as AnchorablePluginConfigObject; + for (int i = 0; i < positions.Count; i++) + { + var pos = GetAnchoredPosition(positions[i], sizes[i], anchorConfig?.Anchor ?? DrawAnchor.Center) + sizes[i]; + maxX = Math.Max(maxX, pos.X); + maxY = Math.Max(maxY, pos.Y); + } + + _maxPos = new Vector2(maxX, maxY); + return (Vector2)_maxPos; + } + } + public void FlagDraggableAreaDirty() + { + _minPos = null; + _maxPos = null; + } + + protected virtual Vector2 GetAnchoredPosition(Vector2 position, Vector2 size, DrawAnchor anchor) + { + return Utils.GetAnchoredPosition(ParentPos() + position, size, anchor); + } + + protected virtual (List, List) ChildrenPositionsAndSizes() + { + return (new List(), new List()); + } + #endregion + } + + public abstract class ParentAnchoredDraggableHudElement : DraggableHudElement + { + public ParentAnchoredDraggableHudElement(MovablePluginConfigObject config, string? displayName = null) + : base(config, displayName) + { + } + + protected virtual bool AnchorToParent { get; } + protected virtual DrawAnchor ParentAnchor { get; } + public AnchorablePluginConfigObject? ParentConfig { get; set; } + + private Vector2? _lastParentPosition = null; + + private bool IsAnchored => AnchorToParent && ParentConfig != null; + + public override Vector2 ParentPos() + { + if (!IsAnchored) + { + return Vector2.Zero; + } + + Vector2 parentAnchoredPos = Utils.GetAnchoredPosition(ParentConfig!.Position, ParentConfig!.Size, ParentConfig!.Anchor); + return Utils.GetAnchoredPosition(parentAnchoredPos, -ParentConfig!.Size, ParentAnchor); + } + + protected override void DrawDraggableArea(Vector2 origin) + { + // if the parent moved, update own draggable area + if (IsAnchored && (_lastParentPosition == null || _lastParentPosition != ParentConfig!.Position)) + { + _windowPositionSet = false; + _minPos = null; + _maxPos = null; + _lastParentPosition = ParentConfig!.Position; + } + + base.DrawDraggableArea(origin); + } + } +} diff --git a/Interface/EnemyList/EnemyListConfig.cs b/Interface/EnemyList/EnemyListConfig.cs new file mode 100644 index 0000000..3dbae82 --- /dev/null +++ b/Interface/EnemyList/EnemyListConfig.cs @@ -0,0 +1,321 @@ +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Enums; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using HSUI.Interface.GeneralElements; +using HSUI.Interface.StatusEffects; +using Dalamud.Bindings.ImGui; +using System.Numerics; + +namespace HSUI.Interface.EnemyList +{ + public enum EnemyListGrowthDirection + { + Down = 0, + Up + } + + [Exportable(false)] + [Section("Enemy List", true)] + [SubSection("General", 0)] + public class EnemyListConfig : MovablePluginConfigObject + { + public new static EnemyListConfig DefaultConfig() + { + var config = new EnemyListConfig(); + Vector2 screenSize = ImGui.GetMainViewport().Size; + config.Position = new Vector2(screenSize.X * 0.2f, -screenSize.Y * 0.2f); + + return config; + } + + [Checkbox("Preview", isMonitored = true)] + [Order(4)] + public bool Preview = false; + + [Combo("Growth Direction", "Down", "Up", spacing = true)] + [Order(20)] + public EnemyListGrowthDirection GrowthDirection = EnemyListGrowthDirection.Down; + + [DragInt("Vertical Padding", min = 0, max = 500)] + [Order(25)] + public int VerticalPadding = 10; + } + + [Exportable(false)] + [DisableParentSettings("Position", "Anchor", "HideWhenInactive")] + [Section("Enemy List", true)] + [SubSection("Health Bar", 0)] + public class EnemyListHealthBarConfig : BarConfig + { + [NestedConfig("Name Label", 70)] + public EditableLabelConfig NameLabel = new EditableLabelConfig(new Vector2(-5, 12), "[name]", DrawAnchor.TopRight, DrawAnchor.BottomRight); + + [NestedConfig("Health Label", 80)] + public EditableLabelConfig HealthLabel = new EditableLabelConfig(new Vector2(30, 0), "[health:percent]%", DrawAnchor.Left, DrawAnchor.Left); + + [NestedConfig("Order Label", 90)] + public DefaultFontLabelConfig OrderLabel = new DefaultFontLabelConfig(new Vector2(5, 0), "", DrawAnchor.Left, DrawAnchor.Left); + + [NestedConfig("Colors", 100)] + public EnemyListHealthBarColorsConfig Colors = new EnemyListHealthBarColorsConfig(); + + [NestedConfig("Change Alpha Based on Range", 110)] + public EnemyListRangeConfig RangeConfig = new EnemyListRangeConfig(); + + [NestedConfig("Use Smooth Transitions", 120)] + public SmoothHealthConfig SmoothHealthConfig = new SmoothHealthConfig(); + + [NestedConfig("Custom Mouseover Area", 130)] + public MouseoverAreaConfig MouseoverAreaConfig = new MouseoverAreaConfig(); + + public new static EnemyListHealthBarConfig DefaultConfig() + { + Vector2 size = new Vector2(180, 40); + + var config = new EnemyListHealthBarConfig(Vector2.Zero, size, new PluginConfigColor(new(233f / 255f, 4f / 255f, 4f / 255f, 100f / 100f))); + config.Colors.ColorByHealth.Enabled = false; + + config.NameLabel.FontID = FontsConfig.DefaultMediumFontKey; + config.HealthLabel.FontID = FontsConfig.DefaultMediumFontKey; + + config.MouseoverAreaConfig.Enabled = false; + + return config; + } + + public EnemyListHealthBarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor, BarDirection fillDirection = BarDirection.Right) + : base(position, size, fillColor, fillDirection) + { + } + } + + [Disableable(false)] + [Exportable(false)] + public class EnemyListHealthBarColorsConfig : PluginConfigObject + { + [NestedConfig("Color Based On Health Value", 30, collapsingHeader = false)] + public ColorByHealthValueConfig ColorByHealth = new ColorByHealthValueConfig(); + + [Checkbox("Highlight When Hovering With Cursor Or Soft Targeting", spacing = true)] + [Order(40)] + public bool ShowHighlight = true; + + [ColorEdit4("Highlight Color")] + [Order(41, collapseWith = nameof(ShowHighlight))] + public PluginConfigColor HighlightColor = new PluginConfigColor(new Vector4(255f / 255f, 255f / 255f, 255f / 255f, 5f / 100f)); + + [Checkbox("Missing Health Color", spacing = true)] + [Order(45)] + public bool UseMissingHealthBar = false; + + [ColorEdit4("Color" + "##MissingHealth")] + [Order(46, collapseWith = nameof(UseMissingHealthBar))] + public PluginConfigColor HealthMissingColor = new PluginConfigColor(new Vector4(255f / 255f, 0f / 255f, 0f / 255f, 100f / 100f)); + + [ColorEdit4("Target Border Color", spacing = true)] + [Order(50)] + public PluginConfigColor TargetBordercolor = new PluginConfigColor(new Vector4(255f / 255f, 255f / 255f, 255f / 255f, 100f / 100f)); + + [DragInt("Target Border Thickness", min = 1, max = 10)] + [Order(51)] + public int TargetBorderThickness = 1; + + [Checkbox("Show Enmity Border Colors", spacing = true)] + [Order(60)] + public bool ShowEnmityBorderColors = true; + + [ColorEdit4("Enmity Leader Color")] + [Order(61, collapseWith = nameof(ShowEnmityBorderColors))] + public PluginConfigColor EnmityLeaderBorderColor = new PluginConfigColor(new Vector4(255f / 255f, 40f / 255f, 40f / 255f, 100f / 100f)); + + [ColorEdit4("Enmity Close To Leader Color")] + [Order(62, collapseWith = nameof(ShowEnmityBorderColors))] + public PluginConfigColor EnmitySecondBorderColor = new PluginConfigColor(new Vector4(255f / 255f, 175f / 255f, 40f / 255f, 100f / 100f)); + } + + [Exportable(false)] + public class EnemyListRangeConfig : PluginConfigObject + { + [DragInt("Range (yalms)", min = 1, max = 500)] + [Order(5)] + public int Range = 30; + + [DragFloat("Alpha", min = 1, max = 100)] + [Order(10)] + public float Alpha = 25; + + + public float AlphaForDistance(int distance, float alpha = 100f) + { + if (!Enabled) + { + return 100f; + } + + return distance > Range ? Alpha : alpha; + } + } + + [DisableParentSettings("FrameAnchor")] + [Exportable(false)] + [Section("Enemy List", true)] + [SubSection("Enmity Icon", 0)] + public class EnemyListEnmityIconConfig : IconConfig + { + [Anchor("Health Bar Anchor")] + [Order(16)] + public DrawAnchor HealthBarAnchor = DrawAnchor.TopLeft; + + public new static EnemyListEnmityIconConfig DefaultConfig() => + new EnemyListEnmityIconConfig(new Vector2(5), new Vector2(24), DrawAnchor.Center, DrawAnchor.TopLeft); + + public EnemyListEnmityIconConfig(Vector2 position, Vector2 size, DrawAnchor anchor, DrawAnchor frameAnchor) + : base(position, size, anchor, frameAnchor) + { + HealthBarAnchor = frameAnchor; + } + } + + [DisableParentSettings("FrameAnchor")] + [Exportable(false)] + [Section("Enemy List", true)] + [SubSection("Sign Icon", 0)] + public class EnemyListSignIconConfig : SignIconConfig + { + [Anchor("Health Bar Anchor")] + [Order(16)] + public DrawAnchor HealthBarAnchor = DrawAnchor.TopLeft; + + [Checkbox("Replace Order Label", help = "When enabled and if the enemy has a sign assigned, the sign icon will be drawn instead of the order label.")] + [Order(30)] + public bool ReplaceOrderLabel = true; + + public new static EnemyListSignIconConfig DefaultConfig() => + new EnemyListSignIconConfig(new Vector2(0), new Vector2(30), DrawAnchor.Center, DrawAnchor.Left); + + public EnemyListSignIconConfig(Vector2 position, Vector2 size, DrawAnchor anchor, DrawAnchor frameAnchor) + : base(position, size, anchor, frameAnchor) + { + HealthBarAnchor = frameAnchor; + } + } + + [DisableParentSettings("AnchorToUnitFrame", "UnitFrameAnchor", "HideWhenInactive", "FillDirection")] + [Exportable(false)] + [Section("Enemy List", true)] + [SubSection("Castbar", 0)] + public class EnemyListCastbarConfig : TargetCastbarConfig + { + public new static EnemyListCastbarConfig DefaultConfig() + { + var size = new Vector2(180, 10); + + var castNameConfig = new LabelConfig(new Vector2(0, 0), "", DrawAnchor.Center, DrawAnchor.Center); + castNameConfig.FontID = FontsConfig.DefaultMediumFontKey; + var castTimeConfig = new NumericLabelConfig(new Vector2(-5, 0), "", DrawAnchor.Right, DrawAnchor.Right); + castTimeConfig.Enabled = false; + castTimeConfig.FontID = FontsConfig.DefaultMediumFontKey; + castTimeConfig.NumberFormat = 1; + + var config = new EnemyListCastbarConfig(Vector2.Zero, size, castNameConfig, castTimeConfig); + config.HealthBarAnchor = DrawAnchor.Bottom; + config.Anchor = DrawAnchor.Bottom; + config.ShowIcon = false; + + return config; + } + + [Anchor("Health Bar Anchor")] + [Order(16)] + public DrawAnchor HealthBarAnchor = DrawAnchor.BottomLeft; + + public EnemyListCastbarConfig(Vector2 position, Vector2 size, LabelConfig castNameConfig, NumericLabelConfig castTimeConfig) + : base(position, size, castNameConfig, castTimeConfig) + { + + } + } + + [Exportable(false)] + [Section("Enemy List", true)] + [SubSection("Buffs", 0)] + public class EnemyListBuffsConfig : EnemyListStatusEffectsListConfig + { + public new static EnemyListBuffsConfig DefaultConfig() + { + var durationConfig = new LabelConfig(new Vector2(0, -4), "", DrawAnchor.Bottom, DrawAnchor.Center); + var stacksConfig = new LabelConfig(new Vector2(-3, 4), "", DrawAnchor.TopRight, DrawAnchor.Center); + stacksConfig.Color = new(Vector4.UnitW); + stacksConfig.OutlineColor = new(Vector4.One); + + var iconConfig = new StatusEffectIconConfig(durationConfig, stacksConfig); + iconConfig.DispellableBorderConfig.Enabled = false; + iconConfig.Size = new Vector2(24, 24); + + var pos = new Vector2(5, 8); + var size = new Vector2(iconConfig.Size.X * 4 + 6, iconConfig.Size.Y); + + var config = new EnemyListBuffsConfig(DrawAnchor.TopRight, pos, size, true, false, false, GrowthDirections.Right | GrowthDirections.Down, iconConfig); + config.Limit = 4; + config.ShowPermanentEffects = true; + config.IconConfig.DispellableBorderConfig.Enabled = false; + + return config; + } + + public EnemyListBuffsConfig(DrawAnchor anchor, Vector2 position, Vector2 size, bool showBuffs, bool showDebuffs, bool showPermanentEffects, + GrowthDirections growthDirections, StatusEffectIconConfig iconConfig) + : base(anchor, position, size, showBuffs, showDebuffs, showPermanentEffects, growthDirections, iconConfig) + { + } + } + + [Exportable(false)] + [Section("Enemy List", true)] + [SubSection("Debuffs", 0)] + public class EnemyListDebuffsConfig : EnemyListStatusEffectsListConfig + { + public new static EnemyListDebuffsConfig DefaultConfig() + { + var durationConfig = new LabelConfig(new Vector2(0, -4), "", DrawAnchor.Bottom, DrawAnchor.Center); + var stacksConfig = new LabelConfig(new Vector2(-3, 4), "", DrawAnchor.TopRight, DrawAnchor.Center); + stacksConfig.Color = new(Vector4.UnitW); + stacksConfig.OutlineColor = new(Vector4.One); + + var iconConfig = new StatusEffectIconConfig(durationConfig, stacksConfig); + iconConfig.Size = new Vector2(24, 24); + + var pos = new Vector2(-5, 8); + var size = new Vector2(iconConfig.Size.X * 4 + 6, iconConfig.Size.Y); + + var config = new EnemyListDebuffsConfig(DrawAnchor.TopLeft, pos, size, false, true, false, GrowthDirections.Left | GrowthDirections.Down, iconConfig); + config.Limit = 4; + config.ShowPermanentEffects = true; + config.IconConfig.DispellableBorderConfig.Enabled = false; + + return config; + } + + public EnemyListDebuffsConfig(DrawAnchor anchor, Vector2 position, Vector2 size, bool showBuffs, bool showDebuffs, bool showPermanentEffects, + GrowthDirections growthDirections, StatusEffectIconConfig iconConfig) + : base(anchor, position, size, showBuffs, showDebuffs, showPermanentEffects, growthDirections, iconConfig) + { + } + } + + public class EnemyListStatusEffectsListConfig : StatusEffectsListConfig + { + [Anchor("Health Bar Anchor")] + [Order(4)] + public DrawAnchor HealthBarAnchor = DrawAnchor.BottomLeft; + + public EnemyListStatusEffectsListConfig(DrawAnchor anchor, Vector2 position, Vector2 size, bool showBuffs, bool showDebuffs, bool showPermanentEffects, + GrowthDirections growthDirections, StatusEffectIconConfig iconConfig) + : base(position, size, showBuffs, showDebuffs, showPermanentEffects, growthDirections, iconConfig) + { + HealthBarAnchor = anchor; + } + } +} diff --git a/Interface/EnemyList/EnemyListHelper.cs b/Interface/EnemyList/EnemyListHelper.cs new file mode 100644 index 0000000..0daf109 --- /dev/null +++ b/Interface/EnemyList/EnemyListHelper.cs @@ -0,0 +1,88 @@ +using Dalamud.Memory; +using HSUI.Helpers; +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Component.GUI; +using System; +using System.Collections.Generic; +using FFXIVClientStructs.FFXIV.Client.UI.Arrays; +using StructsFramework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework; + +namespace HSUI.Interface.EnemyList +{ + public unsafe class EnemyListHelper + { + private List _enemiesData = new List(); + public IReadOnlyCollection EnemiesData => _enemiesData.AsReadOnly(); + public int EnemyCount => _enemiesData.Count; + + public void Update() + { + _enemiesData.Clear(); + + var enemyListNumberInstance = EnemyListNumberArray.Instance(); + var enemyNumberArrayEnemies = enemyListNumberInstance->Enemies; + int enemyCount = *(int*)((byte*)enemyListNumberInstance + 0x04); + //TODO: Change it to the correct property when it lands in CS + //int enemyCount = enemyListNumberInstance->Unk1; + + if(enemyCount == 0) + { + return; + } + + for (int i = 0; i < enemyCount; i++) + { + int entityId = enemyNumberArrayEnemies[i].EntityId; + int? letter = GetEnemyLetter(entityId, i); + int enmityLevel = GetEnmityLevelForIndex(i); + _enemiesData.Add(new EnemyListData(entityId, letter, enmityLevel)); + } + } + + private int? GetEnemyLetter(int objectId, int index) + { + var enemyStringArrayMembers = EnemyListStringArray.Instance()->Members; + if (enemyStringArrayMembers.IsEmpty || enemyStringArrayMembers.Length <= index) + { + return null; + } + + string name = enemyStringArrayMembers[index].EnemyName; + + bool isMarked = Utils.SignIconIDForObjectID((uint)objectId) != null; + char letterSymbol = isMarked && name.Length > 1 ? name[2] : name[0]; + return letterSymbol - 57457; + } + + private int GetEnmityLevelForIndex(int index) + { + // gets enmity level by checking texture in enemy list addon + + AtkUnitBase* enemyList = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("_EnemyList", 1).Address; + if (enemyList == null || enemyList->RootNode == null) { return 0; } + + int id = index == 0 ? 2 : 20000 + index; // makes no sense but it is what it is (blame SE) + AtkResNode* node = enemyList->GetNodeById((uint)id); + if (node == null || node->GetComponent() == null) { return 0; } + + AtkImageNode* imageNode = (AtkImageNode*)node->GetComponent()->UldManager.SearchNodeById(13); + if (imageNode == null) { return 0; } + + return Math.Min(4, imageNode->PartId + 1); + } + } + + public struct EnemyListData + { + public int EntityId; + public int? LetterIndex; + public int EnmityLevel; + + public EnemyListData(int entityId, int? letterIndex, int enmityLevel) + { + EntityId = entityId; + LetterIndex = letterIndex; + EnmityLevel = enmityLevel; + } + } +} diff --git a/Interface/EnemyList/EnemyListHud.cs b/Interface/EnemyList/EnemyListHud.cs new file mode 100644 index 0000000..db6fb03 --- /dev/null +++ b/Interface/EnemyList/EnemyListHud.cs @@ -0,0 +1,428 @@ +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Interface.Textures.TextureWraps; +using HSUI.Config; +using HSUI.Enums; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using HSUI.Interface.GeneralElements; +using HSUI.Interface.StatusEffects; +using Dalamud.Bindings.ImGui; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace HSUI.Interface.EnemyList +{ + public class EnemyListHud : DraggableHudElement, IHudElementWithMouseOver, IHudElementWithPreview + { + private EnemyListConfig Config => (EnemyListConfig)_config; + private EnemyListConfigs Configs; + + private EnemyListHelper _helper = new EnemyListHelper(); + + private List _smoothHPHelpers = new List(); + + private const int MaxEnemyCount = 8; + private List _previewValues = new List(MaxEnemyCount); + + private bool _wasHovering = false; + + private LabelHud _nameLabelHud; + private LabelHud _healthLabelHud; + private LabelHud _orderLabelHud; + private List _castbarHud; + private StatusEffectsListHud _buffsListHud; + private StatusEffectsListHud _debuffsListHud; + + private IDalamudTextureWrap? _iconsTexture => TexturesHelper.GetTextureFromPath("ui/uld/enemylist_hr1.tex"); + + public EnemyListHud(EnemyListConfig config, string displayName) : base(config, displayName) + { + Configs = EnemyListConfigs.GetConfigs(); + + config.ValueChangeEvent += OnConfigPropertyChanged; + + _nameLabelHud = new LabelHud(Configs.HealthBar.NameLabel); + _healthLabelHud = new LabelHud(Configs.HealthBar.HealthLabel); + _orderLabelHud = new LabelHud(Configs.HealthBar.OrderLabel); + + _castbarHud = new List(); + _buffsListHud = new StatusEffectsListHud(Configs.Buffs); + _debuffsListHud = new StatusEffectsListHud(Configs.Debuffs); + + for (int i = 0; i < MaxEnemyCount; i++) + { + _smoothHPHelpers.Add(new SmoothHPHelper()); + _castbarHud.Add(new EnemyListCastbarHud(Configs.CastBar)); + } + + UpdatePreview(); + } + + protected override void InternalDispose() + { + _config.ValueChangeEvent -= OnConfigPropertyChanged; + } + + private void OnConfigPropertyChanged(object sender, OnChangeBaseArgs args) + { + if (args.PropertyName == "Preview") + { + UpdatePreview(); + } + } + + private void UpdatePreview() + { + _previewValues.Clear(); + if (!Config.Preview) { return; } + + Random RNG = new Random((int)ImGui.GetTime()); + + for (int i = 0; i < MaxEnemyCount; i++) + { + _previewValues.Add(RNG.Next(0, 101) / 100f); + } + } + + protected override (List, List) ChildrenPositionsAndSizes() + { + Vector2 size = new Vector2(Configs.HealthBar.Size.X, MaxEnemyCount * Configs.HealthBar.Size.Y + (MaxEnemyCount - 1) * Config.VerticalPadding); + Vector2 pos = Config.GrowthDirection == EnemyListGrowthDirection.Down ? Config.Position : Config.Position - new Vector2(0, size.Y); + + return (new List() { pos + size / 2f }, new List() { size }); + } + + public void StopPreview() + { + Config.Preview = false; + + foreach (EnemyListCastbarHud castbar in _castbarHud) + { + castbar.StopPreview(); + } + + _buffsListHud.StopPreview(); + _debuffsListHud.StopPreview(); + Configs.HealthBar.MouseoverAreaConfig.Preview = false; + Configs.SignIcon.Preview = false; + } + + public void StopMouseover() + { + if (_wasHovering) + { + InputsHelper.Instance.ClearTarget(); + _wasHovering = false; + } + } + + public override void DrawChildren(Vector2 origin) + { + if (!Config.Enabled) { return; } + + _helper.Update(); + + int count = Math.Min(MaxEnemyCount, Config.Preview ? MaxEnemyCount : _helper.EnemyCount); + uint fakeMaxHp = 100000; + + ICharacter? mouseoverTarget = null; + bool hovered = false; + + for (int i = 0; i < count; i++) + { + // hp bar + ICharacter? character = Config.Preview ? null : Plugin.ObjectTable.SearchById((uint)_helper.EnemiesData.ElementAt(i).EntityId) as ICharacter; + + uint currentHp = Config.Preview ? (uint)(_previewValues[i] * fakeMaxHp) : character?.CurrentHp ?? fakeMaxHp; + uint maxHp = Config.Preview ? fakeMaxHp : character?.MaxHp ?? fakeMaxHp; + int enmityLevel = Config.Preview ? Math.Max(4, i + 1) : _helper.EnemiesData.ElementAt(i).EnmityLevel; + + if (Configs.HealthBar.SmoothHealthConfig.Enabled) + { + currentHp = _smoothHPHelpers[i].GetNextHp((int)currentHp, (int)maxHp, Configs.HealthBar.SmoothHealthConfig.Velocity); + } + + int direction = Config.GrowthDirection == EnemyListGrowthDirection.Down ? 1 : -1; + float y = Config.Position.Y + i * direction * Configs.HealthBar.Size.Y + i * direction * Config.VerticalPadding; + Vector2 pos = new Vector2(Config.Position.X, y); + + PluginConfigColor fillColor = GetColor(character, currentHp, maxHp); + PluginConfigColor bgColor = Configs.HealthBar.BackgroundColor; + if (Configs.HealthBar.RangeConfig.Enabled) + { + fillColor = GetDistanceColor(character, fillColor); + bgColor = GetDistanceColor(character, bgColor); + } + Rect background = new Rect(pos, Configs.HealthBar.Size, bgColor); + + PluginConfigColor borderColor = GetBorderColor(character, enmityLevel); + Rect healthFill = BarUtilities.GetFillRect(pos, Configs.HealthBar.Size, Configs.HealthBar.FillDirection, fillColor, currentHp, maxHp); + + BarHud bar = new BarHud( + Configs.HealthBar.ID + $"_{i}", + Configs.HealthBar.DrawBorder, + borderColor, + GetBorderThickness(character), + DrawAnchor.TopLeft, + current: currentHp, + max: maxHp, + shadowConfig: Configs.HealthBar.ShadowConfig, + barTextureName: Configs.HealthBar.BarTextureName, + barTextureDrawMode: Configs.HealthBar.BarTextureDrawMode + ); + + bar.NeedsInputs = true; + bar.SetBackground(background); + bar.AddForegrounds(healthFill); + + if (Configs.HealthBar.Colors.UseMissingHealthBar) + { + Vector2 healthMissingSize = Configs.HealthBar.Size - BarUtilities.GetFillDirectionOffset(healthFill.Size, Configs.HealthBar.FillDirection); + Vector2 healthMissingPos = Configs.HealthBar.FillDirection.IsInverted() ? pos : pos + BarUtilities.GetFillDirectionOffset(healthFill.Size, Configs.HealthBar.FillDirection); + PluginConfigColor? color = Configs.HealthBar.RangeConfig.Enabled ? GetDistanceColor(character, Configs.HealthBar.Colors.HealthMissingColor) : Configs.HealthBar.Colors.HealthMissingColor; + bar.AddForegrounds(new Rect(healthMissingPos, healthMissingSize, color)); + } + + // highlight + var (areaStart, areaEnd) = Configs.HealthBar.MouseoverAreaConfig.GetArea(origin + pos, Configs.HealthBar.Size); + bool isHovering = character != null && ImGui.IsMouseHoveringRect(areaStart, areaEnd); + bool isSoftTarget = character != null && character.EntityId == Plugin.TargetManager.SoftTarget?.EntityId; + if (isHovering || isSoftTarget) + { + if (Configs.HealthBar.Colors.ShowHighlight) + { + Rect highlight = new Rect(pos, Configs.HealthBar.Size, Configs.HealthBar.Colors.HighlightColor); + bar.AddForegrounds(highlight); + } + + mouseoverTarget = character; + hovered = isHovering; + } + + AddDrawActions(bar.GetDrawActions(origin, Configs.HealthBar.StrataLevel)); + + // mouseover area + BarHud? mouseoverAreaBar = Configs.HealthBar.MouseoverAreaConfig.GetBar( + pos, + Configs.HealthBar.Size, + Configs.HealthBar.ID + "_mouseoverArea" + ); + + if (mouseoverAreaBar != null) + { + AddDrawActions(mouseoverAreaBar.GetDrawActions(origin, StrataLevel.HIGHEST)); + } + + // enmity icon + if (_iconsTexture != null && Configs.EnmityIcon.Enabled) + { + var parentPos = Utils.GetAnchoredPosition(origin + pos, -Configs.HealthBar.Size, Configs.EnmityIcon.HealthBarAnchor); + var iconPos = Utils.GetAnchoredPosition(parentPos + Configs.EnmityIcon.Position, Configs.EnmityIcon.Size, Configs.EnmityIcon.Anchor); + int enmityIndex = Config.Preview ? Math.Min(3, i) : _helper.EnemiesData.ElementAt(i).EnmityLevel - 1; + + AddDrawAction(Configs.EnmityIcon.StrataLevel, () => + { + DrawHelper.DrawInWindow(ID + "_enmityIcon", iconPos, Configs.EnmityIcon.Size, false, (drawList) => + { + float w = 48f / _iconsTexture.Width; + float h = 48f / _iconsTexture.Height; + Vector2 uv0 = new Vector2(w * enmityIndex, 0.48f); + Vector2 uv1 = new Vector2(w * (enmityIndex + 1), 0.48f + h); + drawList.AddImage(_iconsTexture.Handle, iconPos, iconPos + Configs.EnmityIcon.Size, uv0, uv1); + }); + }); + } + + // sign icon + uint? signIconId = null; + if (Configs.SignIcon.Enabled) + { + signIconId = Configs.SignIcon.IconID(character); + if (signIconId.HasValue) + { + var parentPos = Utils.GetAnchoredPosition(origin + pos, -Configs.HealthBar.Size, Configs.SignIcon.HealthBarAnchor); + var iconPos = Utils.GetAnchoredPosition(parentPos + Configs.SignIcon.Position, Configs.SignIcon.Size, Configs.SignIcon.Anchor); + + AddDrawAction(Configs.SignIcon.StrataLevel, () => + { + DrawHelper.DrawInWindow(ID + "_signIcon", iconPos, Configs.SignIcon.Size, false, (drawList) => + { + DrawHelper.DrawIcon(signIconId.Value, iconPos, Configs.SignIcon.Size, false, drawList); + }); + }); + } + } + + // labels + string? name = Config.Preview ? "Fake Name" : null; + AddDrawAction(Configs.HealthBar.NameLabel.StrataLevel, () => + { + _nameLabelHud.Draw(origin + pos, Configs.HealthBar.Size, character, name, currentHp, maxHp); + }); + + AddDrawAction(Configs.HealthBar.HealthLabel.StrataLevel, () => + { + _healthLabelHud.Draw(origin + pos, Configs.HealthBar.Size, character, name, currentHp, maxHp); + }); + + if (!signIconId.HasValue || !Configs.SignIcon.ReplaceOrderLabel) + { + int letter = i; + if (!Config.Preview && _helper.EnemiesData.ElementAt(i).LetterIndex.HasValue) + { + letter = _helper.EnemiesData.ElementAt(i).LetterIndex!.Value; + } + + string str = char.ConvertFromUtf32(0xE071 + letter).ToString(); + AddDrawAction(Configs.HealthBar.OrderLabel.StrataLevel, () => + { + Configs.HealthBar.OrderLabel.SetText(str); + _orderLabelHud.Draw(origin + pos, Configs.HealthBar.Size); + }); + } + + // buffs / debuffs + var buffsPos = Utils.GetAnchoredPosition(origin + pos, -Configs.HealthBar.Size, Configs.Buffs.HealthBarAnchor); + AddDrawAction(Configs.Buffs.StrataLevel, () => + { + _buffsListHud.Actor = character; + _buffsListHud.PrepareForDraw(buffsPos); + _buffsListHud.Draw(buffsPos); + }); + + var debuffsPos = Utils.GetAnchoredPosition(origin + pos, -Configs.HealthBar.Size, Configs.Debuffs.HealthBarAnchor); + AddDrawAction(Configs.Debuffs.StrataLevel, () => + { + _debuffsListHud.Actor = character; + _debuffsListHud.PrepareForDraw(debuffsPos); + _debuffsListHud.Draw(debuffsPos); + }); + + // castbar + EnemyListCastbarHud castbar = _castbarHud[i]; + castbar.EnemyListIndex = i; + + var castbarPos = Utils.GetAnchoredPosition(origin + pos, -Configs.HealthBar.Size, Configs.CastBar.HealthBarAnchor); + AddDrawAction(Configs.CastBar.StrataLevel, () => + { + castbar.Actor = character; + castbar.PrepareForDraw(castbarPos); + castbar.Draw(castbarPos); + }); + } + + // mouseover + bool ignoreMouseover = Configs.HealthBar.MouseoverAreaConfig.Enabled && Configs.HealthBar.MouseoverAreaConfig.Ignore; + if (hovered && mouseoverTarget != null) + { + _wasHovering = true; + InputsHelper.Instance.SetTarget(mouseoverTarget, ignoreMouseover); + + // left click + if (InputsHelper.Instance.LeftButtonClicked) + { + Plugin.TargetManager.Target = mouseoverTarget; + } + } + else if (_wasHovering) + { + InputsHelper.Instance.ClearTarget(); + _wasHovering = false; + } + } + + private PluginConfigColor GetColor(ICharacter? character, uint currentHp = 0, uint maxHp = 0) + { + if (Configs.HealthBar.Colors.ColorByHealth.Enabled && (character != null || Config.Preview)) + { + var scale = (float)currentHp / Math.Max(1, maxHp); + return ColorUtils.GetColorByScale(scale, Configs.HealthBar.Colors.ColorByHealth); + } + + return Configs.HealthBar.FillColor; + } + + private PluginConfigColor GetBorderColor(ICharacter? character, int enmityLevel) + { + IGameObject? target = Plugin.TargetManager.SoftTarget ?? Plugin.TargetManager.Target; + if (character != null && target != null && character.EntityId == target.EntityId) + { + return Configs.HealthBar.Colors.TargetBordercolor; + } + + if (!Configs.HealthBar.Colors.ShowEnmityBorderColors) + { + return Configs.HealthBar.BorderColor; + } + + return enmityLevel switch + { + >= 3 => Configs.HealthBar.Colors.EnmityLeaderBorderColor, + >= 1 => Configs.HealthBar.Colors.EnmitySecondBorderColor, + _ => Configs.HealthBar.BorderColor + }; + } + + private int GetBorderThickness(ICharacter? character) + { + IGameObject? target = Plugin.TargetManager.Target; + if (character != null && character == target) + { + return Configs.HealthBar.Colors.TargetBorderThickness; + } + + return Configs.HealthBar.BorderThickness; + } + + private PluginConfigColor GetDistanceColor(ICharacter? character, PluginConfigColor color) + { + byte distance = character != null ? character.YalmDistanceX : byte.MaxValue; + float currentAlpha = color.Vector.W * 100f; + float alpha = Configs.HealthBar.RangeConfig.AlphaForDistance(distance, currentAlpha) / 100f; + + return color.WithAlpha(alpha); + } + } + + #region utils + public struct EnemyListConfigs + { + public EnemyListHealthBarConfig HealthBar; + public EnemyListEnmityIconConfig EnmityIcon; + public EnemyListSignIconConfig SignIcon; + public EnemyListCastbarConfig CastBar; + public EnemyListBuffsConfig Buffs; + public EnemyListDebuffsConfig Debuffs; + + public EnemyListConfigs( + EnemyListHealthBarConfig healthBar, + EnemyListEnmityIconConfig enmityIcon, + EnemyListSignIconConfig signIcon, + EnemyListCastbarConfig castBar, + EnemyListBuffsConfig buffs, + EnemyListDebuffsConfig debuffs) + { + HealthBar = healthBar; + EnmityIcon = enmityIcon; + SignIcon = signIcon; + CastBar = castBar; + Buffs = buffs; + Debuffs = debuffs; + } + + public static EnemyListConfigs GetConfigs() + { + return new EnemyListConfigs( + ConfigurationManager.Instance.GetConfigObject(), + ConfigurationManager.Instance.GetConfigObject(), + ConfigurationManager.Instance.GetConfigObject(), + ConfigurationManager.Instance.GetConfigObject(), + ConfigurationManager.Instance.GetConfigObject(), + ConfigurationManager.Instance.GetConfigObject() + ); + } + } + #endregion +} diff --git a/Interface/GeneralElements/ActionBarsHud.cs b/Interface/GeneralElements/ActionBarsHud.cs new file mode 100644 index 0000000..b31a350 --- /dev/null +++ b/Interface/GeneralElements/ActionBarsHud.cs @@ -0,0 +1,1394 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Game.Gui; +using HSUI.Config; +using HSUI.Enums; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using Dalamud.Bindings.ImGui; +using Dalamud.Interface.Utility; +using Dalamud.Utility; +using Lumina.Excel; +using Lumina.Excel.Sheets; +using Lumina.Text.ReadOnly; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Client.UI.Misc; +using FFXIVClientStructs.FFXIV.Component.GUI; +using LuminaAction = Lumina.Excel.Sheets.Action; + +namespace HSUI.Interface.GeneralElements +{ + public class ActionBarsHud : DraggableHudElement, IHudElementWithActor, IHudElementWithVisibilityConfig + { + private HotbarBarConfig Config => (HotbarBarConfig)_config; + + private bool _wasDragging; + private uint _dragId; + private RaptureHotbarModule.HotbarSlotType _dragSlotType; + private int _pendingSlotIconIndex = -1; + private uint _pendingSlotIconId; + private int _pendingSlotIconFramesLeft; + /// When we process a game drop on a slot, we must suppress the InvisibleButton "click" that would trigger ExecuteSlot. + private int _lastFrameDroppedOnSlot = -1; + /// When we had a game drag and the user released, suppress all slot clicks this frame (release is not a click). + private bool _suppressSlotClicksAfterDragRelease; + + private const string SlotPayloadType = "HSUI_HOTBAR_SLOT"; + private const int SlotsPerBar = 12; + + private static int _imGuiDragSourceSlotId = -1; + private static bool _anyHotbarAcceptedDrop; + private static int _lastOverlayFrame = -1; + /// Deferred clear when release-outside: set when no overlay accepted, executed next frame. + private static int _pendingReleaseOutsideSlotId = -1; + /// To avoid PICKUP log spam: only log when we first start dragging a new slot. + private static int _lastLoggedPickupSlotId = -1; + + /// Encode hotbar (1-10) and slot (0-11) into internal slot id (0-119). + private static int ToSlotId(int hotbarIndex, int slotIndex) + { + int bar = Math.Clamp(hotbarIndex, 1, 10) - 1; + int slot = Math.Clamp(slotIndex, 0, 11); + return bar * SlotsPerBar + slot; + } + + /// Decode internal slot id to hotbar (1-10) and slot (0-11). Returns (-1,-1) if invalid. + private static (int hotbarIndex, int slotIndex) FromSlotId(int slotId) + { + if (slotId < 0 || slotId >= 10 * SlotsPerBar) return (-1, -1); + return (slotId / SlotsPerBar + 1, slotId % SlotsPerBar); + } + + [StructLayout(LayoutKind.Sequential)] + private struct SlotDragPayload + { + public int SlotId; + } + + /// + /// Visibility for HSUI Action Bars is driven by Visibility → Hotbars (Hotbar 1–10), not the per-element config. + /// + public VisibilityConfig VisibilityConfig => GetHotbarVisibilityConfig(); + + private VisibilityConfig GetHotbarVisibilityConfig() + { + var hotbars = ConfigurationManager.Instance?.GetConfigObject(); + if (hotbars == null) return Config.VisibilityConfig; + var list = hotbars.GetHotbarConfigs(); + int idx = Config.HotbarIndex - 1; + if (idx < 0 || idx >= list.Count) return Config.VisibilityConfig; + return list[idx]; + } + + public IGameObject? Actor { get; set; } + + public ActionBarsHud(HotbarBarConfig config, string displayName) : base(config, displayName) { } + + protected override (List, List) ChildrenPositionsAndSizes() + { + var size = BarSize(); + return (new List { Config.Position }, new List { size }); + } + + private Vector2 BarSize() + { + var (cols, rows) = 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); + } + + private Vector2 GetSlotPosition(int slotIndex, Vector2 slotSize, int pad) + { + var (cols, _) = Config.GetLayoutGrid(); + int col = slotIndex % cols; + int row = slotIndex / cols; + return new Vector2(col * (slotSize.X + pad), row * (slotSize.Y + pad)); + } + + public override void DrawChildren(Vector2 origin) + { + if (!Config.Enabled || Actor == null) + return; + + if (ActionBarsManager.Instance == null) + return; + + _lastFrameDroppedOnSlot = -1; + _suppressSlotClicksAfterDragRelease = false; + + var barSize = BarSize(); + var topLeft = Utils.GetAnchoredPosition(origin + Config.Position, barSize, Config.Anchor); + var slotSize = Config.SlotSize; + int pad = Config.SlotPadding; + bool showCd = Config.ShowCooldownOverlay; + bool showCdNumbers = Config.ShowCooldownNumbers; + bool showBorder = Config.ShowBorder; + + AddDrawAction(Config.StrataLevel, () => + { + HandleDragDrop(topLeft, barSize, slotSize, pad); + // Refresh slot data after placement so overwrite shows correctly + var slots = ActionBarsManager.Instance.GetSlotData(Config.HotbarIndex, Config.SlotCount); + + void DrawBarContent(ImDrawListPtr drawList) + { + using (FontsManager.Instance.PushFont(FontsConfig.DefaultSmallFontKey)) + { + for (int i = 0; i < slots.Count; i++) + { + var slot = slots[i]; + uint displayIconId = slot.IconId; + bool isEmpty = slot.IsEmpty; + bool usePending = _pendingSlotIconIndex == i && _pendingSlotIconId != 0; + if (usePending) + { + displayIconId = _pendingSlotIconId; + isEmpty = false; + } + + var slotOffset = GetSlotPosition(i, slotSize, pad); + var pos = topLeft + slotOffset; + var size = new Vector2(slotSize.X, slotSize.Y); + bool hovered = !isEmpty && ImGui.IsMouseHoveringRect(pos, pos + size); + + if (isEmpty) + { + drawList.AddRectFilled(pos, pos + size, 0x22000000); + if (showBorder) + drawList.AddRect(pos, pos + size, 0xFF444444); + } + else + { + uint color = hovered ? 0xFFFFFFFF : (slot.IsUsable ? 0xFFFFFFFF : 0xAA888888); + bool useHighlightBorder = hovered && showBorder; + bool isComboNext = Config.ShowComboHighlight && IsComboNextAction(slot); + DrawHelper.DrawIcon(displayIconId, pos, size, showBorder && !useHighlightBorder && !isComboNext, color, drawList); + + if (useHighlightBorder) + drawList.AddRect(pos, pos + size, 0xFF88CCFF, 0, ImDrawFlags.None, 2); + else if (isComboNext) + { + const uint gold = 0xFFFFD700; + for (int g = 5; g >= 1; g--) + drawList.AddRect(pos - new Vector2(g, g), pos + size + new Vector2(g, g), (uint)((byte)(70 * (6 - g)) << 24 | (gold & 0xFFFFFF)), 0, ImDrawFlags.None, 2f); + drawList.AddRect(pos, pos + size, gold, 0, ImDrawFlags.None, 4f); + } + + if (showCd && slot.CooldownPercent > 0 && _pendingSlotIconIndex != i) + { + float total = 100f; + float elapsed = total - slot.CooldownPercent; + DrawHelper.DrawIconCooldown(pos, size, elapsed, total, drawList); + if (showCdNumbers && slot.CooldownSecondsLeft > 0) + { + string cdText = slot.CooldownSecondsLeft.ToString(); + Vector2 textSize = ImGui.CalcTextSize(cdText); + Vector2 textPos = pos + (size - textSize) * 0.5f; + DrawHelper.DrawOutlinedText(cdText, textPos, 0xFFFFFFFF, 0xFF000000, drawList, 1); + } + } + } + + if (Config.ShowSlotNumbers) + { + string label = !string.IsNullOrWhiteSpace(slot.KeybindHint) + ? slot.KeybindHint + : ActionBarsManager.GetDefaultKeybindLabel(Config.HotbarIndex, i); + Vector2 labelSize = ImGui.CalcTextSize(label); + Vector2 labelPos = new Vector2(pos.X + size.X - labelSize.X - 2, pos.Y + size.Y - labelSize.Y - 2); + DrawHelper.DrawOutlinedText(label, labelPos, 0xFFFFFFFF, 0xFF000000, drawList, 1); + } + } + } + } + + if (IsGameDragging()) + { + var mp = ImGui.GetMousePos(); + float hole = 32f; + var cursorRect = new ClipRect(mp - new Vector2(hole), mp + new Vector2(hole)); + var barRect = new ClipRect(topLeft, topLeft + barSize); + var inverted = ClipRectsHelper.GetInvertedClipRects(cursorRect); + var drawList = ImGui.GetWindowDrawList(); + for (int i = 0; i < inverted.Length; i++) + { + var inter = barRect.Intersect(inverted[i]); + if (!inter.HasValue) continue; + var r = inter.Value; + ImGui.PushClipRect(r.Min, r.Max, false); + DrawBarContent(drawList); + ImGui.PopClipRect(); + } + } + else + { + DrawHelper.DrawInWindow(ID + "_actionbars", topLeft, barSize, false, DrawBarContent); + } + }); + + // Overlay at HIGHEST so it captures input above the bar; required for icon swap. + // Use unique ID per hotbar to avoid any cross-bar conflicts (bar 2–10 had input capture issues). + AddDrawAction(StrataLevel.HIGHEST, () => + { + var slots = ActionBarsManager.Instance.GetSlotData(Config.HotbarIndex, Config.SlotCount); + var flags = ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoBackground + | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoSavedSettings + | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoBringToFrontOnFocus + | ImGuiWindowFlags.NoNav; + + ImGuiHelpers.ForceNextWindowMainViewport(); + ImGui.SetNextWindowPos(topLeft); + ImGui.SetNextWindowSize(barSize); + string overlayId = $"HSUI_Hotbar{Config.HotbarIndex}_input"; + if (!ImGui.Begin(overlayId, flags)) + { + ImGui.End(); + return; + } + + int frame = ImGui.GetFrameCount(); + if (frame != _lastOverlayFrame) + { + _lastOverlayFrame = frame; + _anyHotbarAcceptedDrop = false; + // Process deferred release-outside clear from previous frame (must run before overlays) + if (_pendingReleaseOutsideSlotId >= 0 && GetHotbarsConfig()?.GeneralOptions?.EnableReleaseOutsideToClear != false) + { + var (bar, slot) = FromSlotId(_pendingReleaseOutsideSlotId); + if (bar >= 1 && slot >= 0) + ActionBarsManager.Instance?.ClearSlot(bar, slot); + _pendingReleaseOutsideSlotId = -1; + } + } + + bool acceptedDrop = false; + for (int i = 0; i < Config.SlotCount; i++) + { + var slotOffset = GetSlotPosition(i, slotSize, pad); + var slotPos = topLeft + slotOffset; + ImGui.SetCursorScreenPos(slotPos); + bool clicked = ImGui.InvisibleButton($"##{overlayId}_slot_{i}", slotSize); + bool isHovered = ImGui.IsItemHovered(); + bool isActive = ImGui.IsItemActive(); + + bool shiftHeld = ImGui.GetIO().KeyShift; + + int targetSlotId = ToSlotId(Config.HotbarIndex, i); + + if (GetHotbarsConfig()?.GeneralOptions?.EnableShiftDragToRearrange != false && ImGui.BeginDragDropTarget()) + { + var payload = ImGui.AcceptDragDropPayload(SlotPayloadType); + if (payload != ImGuiPayloadPtr.Null && payload.IsDataType(SlotPayloadType) && TryGetSlotPayload(payload, out var src)) + { + acceptedDrop = true; + _anyHotbarAcceptedDrop = true; + _imGuiDragSourceSlotId = -1; + _pendingReleaseOutsideSlotId = -1; + _lastLoggedPickupSlotId = -1; + if (src.SlotId != targetSlotId) + { + var (srcBar, srcSlot) = FromSlotId(src.SlotId); + if (srcBar >= 1 && srcSlot >= 0) + { + ActionBarsManager.Instance?.SwapSlots(srcBar, srcSlot, Config.HotbarIndex, i, + Config.DebugDragDrop ? s => Plugin.Logger.Information($"[HSUI DragDrop DBG] {s}") : null); + } + } + } + ImGui.EndDragDropTarget(); + } + + if (GetHotbarsConfig()?.GeneralOptions?.EnableShiftDragToRearrange != false && !IsGameDragging() && shiftHeld && i < slots.Count && !slots[i].IsEmpty && isActive && ImGui.IsMouseDragging(ImGuiMouseButton.Left)) + { + if (ImGui.BeginDragDropSource(ImGuiDragDropFlags.None)) + { + _imGuiDragSourceSlotId = targetSlotId; + if (Config.DebugDragDrop && _lastLoggedPickupSlotId != targetSlotId) + { + _lastLoggedPickupSlotId = targetSlotId; + Plugin.Logger.Information($"[HSUI DragDrop DBG] Shift+drag PICKUP: bar={Config.HotbarIndex} slot={i} slotId={targetSlotId}"); + } + SetSlotPayload(new SlotDragPayload { SlotId = targetSlotId }); + var icon = TexturesHelper.GetTextureFromIconId(slots[i].IconId); + if (icon != null) + ImGui.Image(icon.Handle, new Vector2(32, 32)); + ImGui.EndDragDropSource(); + } + } + + if (clicked && !acceptedDrop && !ImGui.IsMouseDragging(ImGuiMouseButton.Left) && i < slots.Count && !slots[i].IsEmpty) + { + if (_suppressSlotClicksAfterDragRelease || i == _lastFrameDroppedOnSlot) + { + if (Config.DebugDragDrop) + Plugin.Logger.Information($"[HSUI DragDrop DBG] Suppressed ExecuteSlot slot={i} suppressAfterDrag={_suppressSlotClicksAfterDragRelease} lastDroppedOn={_lastFrameDroppedOnSlot}"); + _suppressSlotClicksAfterDragRelease = false; + _lastFrameDroppedOnSlot = -1; + } + else if (shiftHeld) + { + ActionBarsManager.Instance?.ClearSlot(Config.HotbarIndex, i); + } + else + { + if (Config.DebugDragDrop) + Plugin.Logger.Information($"[HSUI DragDrop DBG] ExecuteSlot hotbar={Config.HotbarIndex} slot={i}"); + ActionBarsManager.Instance?.ExecuteSlot(Config.HotbarIndex, i); + } + } + + if (Config.ShowTooltips && isHovered && i < slots.Count) + { + var slot = slots[i]; + if (!slot.IsEmpty) + { + var (title, text) = GetSlotTooltip(slot); + if (!string.IsNullOrEmpty(title) || !string.IsNullOrEmpty(text)) + { + string body = string.IsNullOrEmpty(text) ? title : text; + TooltipsHelper.Instance.ShowTooltipOnCursor(body, title, slot.ActionId, ""); + if (IsTooltipDebugEnabled()) + Plugin.Logger.Information($"[HSUI Tooltip DBG] ActionBar tooltip (main overlay): slot={i} title='{title}'"); + } + } + } + } + + // Release-outside clear: defer to next frame so we run after all overlays have had a chance to accept + if (GetHotbarsConfig()?.GeneralOptions?.EnableReleaseOutsideToClear != false && ImGui.GetIO().KeyShift && _imGuiDragSourceSlotId >= 0 && + ImGui.IsMouseReleased(ImGuiMouseButton.Left) && !_anyHotbarAcceptedDrop) + { + if (Config.DebugDragDrop) + { + var (bar, slot) = FromSlotId(_imGuiDragSourceSlotId); + Plugin.Logger.Information($"[HSUI DragDrop DBG] Release outside: defer clear bar={bar} slot={slot}"); + } + _pendingReleaseOutsideSlotId = _imGuiDragSourceSlotId; + _imGuiDragSourceSlotId = -1; + _lastLoggedPickupSlotId = -1; + } + + ImGui.End(); + }); + } + + /// Tooltip-only overlay when click overlay is skipped (proxy on / HUD not locked). Uses NoInputs so it doesn't capture clicks. + private void DrawTooltipOnlyOverlay(Vector2 topLeft, Vector2 barSize, Vector2 slotSize, int pad) + { + if (!Config.ShowTooltips) + return; + var slots = ActionBarsManager.Instance?.GetSlotData(Config.HotbarIndex, Config.SlotCount); + if (slots == null) + return; + var flags = ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoBackground + | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoSavedSettings + | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoBringToFrontOnFocus + | ImGuiWindowFlags.NoInputs; + ImGuiHelpers.ForceNextWindowMainViewport(); + ImGui.SetNextWindowPos(topLeft); + ImGui.SetNextWindowSize(barSize); + if (!ImGui.Begin(ID + "_actionbars_tooltip_only", flags)) + { + ImGui.End(); + return; + } + for (int i = 0; i < Config.SlotCount && i < slots.Count; i++) + { + var slotOffset = GetSlotPosition(i, slotSize, pad); + var slotPos = topLeft + slotOffset; + if (ImGui.IsMouseHoveringRect(slotPos, slotPos + slotSize)) + { + var slot = slots[i]; + if (!slot.IsEmpty) + { + var (title, text) = GetSlotTooltip(slot); + if (!string.IsNullOrEmpty(title) || !string.IsNullOrEmpty(text)) + { + string body = string.IsNullOrEmpty(text) ? title : text; + TooltipsHelper.Instance.ShowTooltipOnCursor(body, title, slot.ActionId, ""); + if (IsTooltipDebugEnabled()) + Plugin.Logger.Information($"[HSUI Tooltip DBG] ActionBar tooltip (overlay): slot={i} title='{title}'"); + } + } + break; + } + } + ImGui.End(); + } + + private static bool IsTooltipDebugEnabled() + { + try + { + return ConfigurationManager.Instance?.GetConfigObject()?.DebugTooltips == true; + } + catch { return false; } + } + + private static (uint LastActionId, string? LastDesc, double LastTime) _tooltipDebugLogState; + private static bool ShouldLogTooltipDebug(uint actionId, string name, string? desc) + { + double now = ImGui.GetTime(); + var (lastId, lastDesc, lastTime) = _tooltipDebugLogState; + if (actionId == lastId && desc == lastDesc && now - lastTime < 1f) + return false; + _tooltipDebugLogState = (actionId, desc, now); + return true; + } + + private static bool TryGetDebug() + { + try + { + var configs = ConfigurationManager.Instance?.GetObjects(); + return configs != null && configs.Exists(c => c.DebugDragDrop); + } + catch { return false; } + } + + private static (int Int1, int Int2, double LastTime) _lastItemPayloadLog; + private static bool ShouldLogItemPayload(int int1, int int2) + { + double now = ImGui.GetTime(); + var last = _lastItemPayloadLog; + if (int1 == last.Int1 && int2 == last.Int2 && now - last.LastTime < 0.5) + return false; + _lastItemPayloadLog = (int1, int2, now); + return true; + } + + private static (int Int1, int Int2, double LastTime) _lastMacroPayloadLog; + private static bool ShouldLogMacroPayload(int rawInt1, int rawInt2) + { + double now = ImGui.GetTime(); + var last = _lastMacroPayloadLog; + if (rawInt1 == last.Int1 && rawInt2 == last.Int2 && now - last.LastTime < 2.0) + return false; + _lastMacroPayloadLog = (rawInt1, rawInt2, now); + return true; + } + + private static HotbarsConfig? GetHotbarsConfig() => + ConfigurationManager.Instance?.GetConfigObject(); + + private static (int Type, int Int1, int Int2, double LastTime) _lastDragNoPayloadLog; + private static unsafe bool ShouldLogDragNoPayload(AtkDragDropManager* dm) + { + if (dm == null) return false; + var dd = dm->DragDrop1; + int type = dd != null ? (int)dd->DragDropType : 0; + int i1 = dm->PayloadContainer.Int1; + int i2 = dm->PayloadContainer.Int2; + double now = ImGui.GetTime(); + var last = _lastDragNoPayloadLog; + if (type == last.Type && i1 == last.Int1 && i2 == last.Int2 && now - last.LastTime < 0.5) + return false; + _lastDragNoPayloadLog = (type, i1, i2, now); + return true; + } + + private static unsafe bool IsGameDragging() + { + var stage = AtkStage.Instance(); + if (stage == null) return false; + var dm = (AtkDragDropManager*)Unsafe.AsPointer(ref stage->DragDropManager); + return dm->IsDragging; + } + + private static unsafe bool TryGetSlotPayload(ImGuiPayloadPtr payload, out SlotDragPayload src) + { + if (payload.Data == null || payload.DataSize < sizeof(SlotDragPayload)) + { + src = default; + return false; + } + src = *(SlotDragPayload*)payload.Data; + return true; + } + + private static void SetSlotPayload(SlotDragPayload pl) + { + var type = new ImU8String(SlotPayloadType); + var data = MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref pl, 1)); + ImGui.SetDragDropPayload(type, data, ImGuiCond.None); + type.Recycle(); + } + + private static bool IsComboNextAction(ActionBarsManager.SlotInfo slot) + { + if (slot.IsEmpty || slot.ActionId == 0) return false; + try + { + var slotType = slot.SlotType; + var actionType = slotType switch + { + RaptureHotbarModule.HotbarSlotType.Action => ActionType.Action, + RaptureHotbarModule.HotbarSlotType.GeneralAction => ActionType.GeneralAction, + RaptureHotbarModule.HotbarSlotType.CraftAction => ActionType.CraftAction, + RaptureHotbarModule.HotbarSlotType.PetAction => ActionType.PetAction, + _ => (ActionType?)null + }; + if (!actionType.HasValue) return false; + return SpellHelper.Instance.IsActionHighlighted(slot.ActionId, actionType.Value); + } + catch { return false; } + } + + private unsafe void HandleDragDrop(Vector2 topLeft, Vector2 barSize, Vector2 slotSize, int pad) + { + bool debug = Config.DebugDragDrop; + + // NOTE: Release-outside clear is done in the overlay only. HandleDragDrop runs before overlay + // processes AcceptDragDropPayload, so we must not clear here or we'd clear the source before + // the drop is processed (breaking swap/move when dropping on a slot). + + if (_pendingSlotIconIndex >= 0 && _pendingSlotIconFramesLeft > 0) + { + _pendingSlotIconFramesLeft--; + if (_pendingSlotIconFramesLeft <= 0) + _pendingSlotIconIndex = -1; + } + else if (_pendingSlotIconIndex >= 0) + { + _pendingSlotIconIndex = -1; + } + + var stage = AtkStage.Instance(); + if (stage == null) return; + var dm = (AtkDragDropManager*)Unsafe.AsPointer(ref stage->DragDropManager); + bool dragging = dm->IsDragging; + + if (dragging) + { + if (GetHotbarsConfig()?.GeneralOptions?.EnableDragDropFromGame != false && TryGetDragPayload(out var slotType, out var id)) + { + _dragSlotType = slotType; + _dragId = id; + if (!_wasDragging && debug) + Plugin.Logger.Information($"[HSUI DragDrop] Game drag detected, payload: type={slotType} id={id}"); + _wasDragging = true; + + var mp = ImGui.GetMousePos(); + int idx = GetSlotIndexAtPosition(topLeft, barSize, slotSize, pad, Config.SlotCount, Config.GetLayoutGrid().Cols, mp, false); + if (idx >= 0) + { + InputsHelper.SuppressExecuteSlotByIdAfterDrop((uint)(Config.HotbarIndex - 1), (uint)idx, 500); + } + + if (ImGui.IsMouseReleased(ImGuiMouseButton.Left)) + { + int releaseIdx = GetSlotIndexAtPosition(topLeft, barSize, slotSize, pad, Config.SlotCount, Config.GetLayoutGrid().Cols, mp, debug); + if (releaseIdx >= 0 && slotType != RaptureHotbarModule.HotbarSlotType.Empty && (id != 0 || slotType == RaptureHotbarModule.HotbarSlotType.Macro)) + { + if (!ImGui.GetIO().KeyShift) + { + try + { + var module = RaptureHotbarModule.Instance(); + if (module != null && module->ModuleReady) + { + uint barId = (uint)(Config.HotbarIndex - 1); + ref var displayBar = ref module->StandardHotbars[(int)barId]; + var displaySlot = displayBar.GetHotbarSlot((uint)releaseIdx); + if (displaySlot != null) + { + displaySlot->Set(slotType, id); + displaySlot->LoadIconId(); + } + module->SetAndSaveSlot(barId, (uint)releaseIdx, slotType, id); + uint iconId = GetIconIdForPayload(slotType, id); + if (iconId == 0) + { + var bar = module->StandardHotbars[(int)barId]; + var writtenSlot = bar.GetHotbarSlot((uint)releaseIdx); + if (writtenSlot != null) + iconId = (uint)writtenSlot->IconId; + } + if (iconId != 0) + { + _pendingSlotIconIndex = releaseIdx; + _pendingSlotIconId = iconId; + _pendingSlotIconFramesLeft = 300; + } + _lastFrameDroppedOnSlot = releaseIdx; + InputsHelper.SuppressExecuteSlotByIdAfterDrop(barId, (uint)releaseIdx, 300); + if (slotType == RaptureHotbarModule.HotbarSlotType.Action || + slotType == RaptureHotbarModule.HotbarSlotType.GeneralAction || + slotType == RaptureHotbarModule.HotbarSlotType.CraftAction || + slotType == RaptureHotbarModule.HotbarSlotType.PetAction) + InputsHelper.SuppressUseActionAfterDrop(id, 300); + if (debug) + Plugin.Logger.Information($"[HSUI DragDrop] Placed {slotType} id={id} on slot {releaseIdx}"); + } + } + catch (Exception ex) + { + Plugin.Logger.Warning($"[HSUI DragDrop] Drop failed: {ex.Message}"); + } + } + try { dm->CancelDragDrop(true, true); } + catch { } + } + else + { + try { dm->CancelDragDrop(true, true); } + catch { } + } + _wasDragging = false; + } + } + else + { + _dragSlotType = RaptureHotbarModule.HotbarSlotType.Empty; + _dragId = 0; + _wasDragging = true; + if (ImGui.IsMouseReleased(ImGuiMouseButton.Left)) + { + try { dm->CancelDragDrop(true, true); } + catch { } + _wasDragging = false; + } + if (debug && ShouldLogDragNoPayload(dm)) + { + try + { + var dd = dm->DragDrop1; + if (dd != null) + Plugin.Logger.Information($"[HSUI DragDrop DBG] Drag but no payload (clearing cache): DragDropType={dd->DragDropType} Int1={dm->PayloadContainer.Int1} Int2={dm->PayloadContainer.Int2} HoveredItem={Plugin.GameGui?.HoveredItem ?? 0}"); + } + catch { } + } + } + return; + } + + if (_wasDragging) + { + _suppressSlotClicksAfterDragRelease = true; + var mp = ImGui.GetMousePos(); + int idx = GetSlotIndexAtPosition(topLeft, barSize, slotSize, pad, Config.SlotCount, Config.GetLayoutGrid().Cols, mp, debug); + + if (idx >= 0) + { + var slotType = _dragSlotType; + var id = _dragId; + // Only use payload we captured during the drag. Do NOT fall back to TryGetDragPayload here: + // after we eat LBUTTONUP, the game drag is cancelled and HoveredAction/HoveredItem can be + // stale (e.g. from macro menu UI), causing wrong placements (macro drag -> GeneralAction placed). + // If we never got valid payload (e.g. Macro Int2=0), skip the place. + bool hasValidPayload = slotType != RaptureHotbarModule.HotbarSlotType.Empty && + (id != 0 || slotType == RaptureHotbarModule.HotbarSlotType.Macro); + + if (hasValidPayload) + { + bool shiftHeld = ImGui.GetIO().KeyShift; + if (shiftHeld) + { + try { dm->CancelDragDrop(true, true); } + catch (Exception ex) { Plugin.Logger.Warning($"[HSUI DragDrop] CancelDragDrop: {ex.Message}"); } + ActionBarsManager.Instance?.ClearSlot(Config.HotbarIndex, idx); + if (debug) + Plugin.Logger.Information($"[HSUI DragDrop] Shift+release: cleared slot {idx}"); + _lastFrameDroppedOnSlot = idx; + _wasDragging = false; + return; + } + + try + { + var module = RaptureHotbarModule.Instance(); + if (module != null && module->ModuleReady) + { + uint barId = (uint)(Config.HotbarIndex - 1); + ref var displayBar = ref module->StandardHotbars[(int)barId]; + var displaySlot = displayBar.GetHotbarSlot((uint)idx); + if (displaySlot != null) + { + displaySlot->Set(slotType, id); + displaySlot->LoadIconId(); + } + module->SetAndSaveSlot(barId, (uint)idx, slotType, id); + try { dm->CancelDragDrop(true, true); } + catch (Exception ex2) { Plugin.Logger.Warning($"[HSUI DragDrop] CancelDragDrop: {ex2.Message}"); } + + uint iconId = GetIconIdForPayload(slotType, id); + if (iconId == 0) + { + var bar = module->StandardHotbars[(int)barId]; + var writtenSlot = bar.GetHotbarSlot((uint)idx); + if (writtenSlot != null) + iconId = (uint)writtenSlot->IconId; + } + if (iconId != 0) + { + _pendingSlotIconIndex = idx; + _pendingSlotIconId = iconId; + _pendingSlotIconFramesLeft = 300; + } + _lastFrameDroppedOnSlot = idx; + InputsHelper.SuppressExecuteSlotByIdAfterDrop(barId, (uint)idx, 300); + if (slotType == RaptureHotbarModule.HotbarSlotType.Action || + slotType == RaptureHotbarModule.HotbarSlotType.GeneralAction || + slotType == RaptureHotbarModule.HotbarSlotType.CraftAction || + slotType == RaptureHotbarModule.HotbarSlotType.PetAction) + InputsHelper.SuppressUseActionAfterDrop(id, 300); + if (debug) + Plugin.Logger.Information($"[HSUI DragDrop] Placed {slotType} id={id} on slot {idx}"); + } + } + catch (Exception ex) + { + Plugin.Logger.Warning($"[HSUI DragDrop] Drop processing failed: {ex.Message}\n{ex.StackTrace}"); + } + } + else if (debug) + { + Plugin.Logger.Information($"[HSUI DragDrop] Release over slot {idx} but invalid payload: type={slotType} id={id}"); + } + } + else if (debug) + { + Plugin.Logger.Information($"[HSUI DragDrop] Release outside bar area (idx={idx})"); + } + _wasDragging = false; + } + else if (!dragging) + { + _wasDragging = false; + } + } + + private const float DropMargin = 24f; + + private static int GetSlotIndexAtPosition(Vector2 topLeft, Vector2 barSize, Vector2 slotSize, int pad, int slotCount, int cols, Vector2 pos, bool debug = false) + { + // topLeft and InvisibleButtons use the same coordinate system as ImGui.SetCursorScreenPos/GetMousePos. + // Use strict bar bounds (no extended reach) so only one hotbar matches when bars are stacked. + var viewport = ImGui.GetMainViewport(); + Vector2 adjPos = pos - viewport.Pos; + + int TryHit(Vector2 p) + { + float barMinX = topLeft.X; + float barMaxX = topLeft.X + barSize.X; + float barMinY = topLeft.Y; + float barMaxY = topLeft.Y + barSize.Y; + if (p.X < barMinX || p.X >= barMaxX || p.Y < barMinY || p.Y >= barMaxY) + return -1; + int best = -1; + float bestDistSq = float.MaxValue; + for (int i = 0; i < slotCount; i++) + { + int col = i % cols; + int row = i / cols; + float cx = topLeft.X + col * (slotSize.X + pad) + slotSize.X * 0.5f; + float cy = topLeft.Y + row * (slotSize.Y + pad) + slotSize.Y * 0.5f; + float dx = p.X - cx, dy = p.Y - cy; + float distSq = dx * dx + dy * dy; + float slotHalfW = slotSize.X * 0.5f + DropMargin; + float slotHalfH = slotSize.Y * 0.5f + DropMargin; + bool inSlot = System.Math.Abs(dx) <= slotHalfW && System.Math.Abs(dy) <= slotHalfH; + if (inSlot && distSq < bestDistSq) { bestDistSq = distSq; best = i; } + } + return best; + } + + int idx = TryHit(pos); + if (idx < 0 && adjPos != pos) + idx = TryHit(adjPos); + + if (debug) + Plugin.Logger.Information($"[HSUI DragDrop DBG] HitTest: rawMouse=({pos.X:F1},{pos.Y:F1}) viewport.Pos=({viewport.Pos.X:F1},{viewport.Pos.Y:F1}) adjPos=({adjPos.X:F1},{adjPos.Y:F1}) slotIdx={idx}"); + return idx; + } + + private static unsafe uint GetActiveHotbarClassJobId() + { + var module = RaptureHotbarModule.Instance(); + if (module != null) + return module->ActiveHotbarClassJobId; + var player = Plugin.ObjectTable?.LocalPlayer; + return player != null ? (uint)player.ClassJob.RowId : 0; + } + + /// + /// Gets drag payload from game/Dalamud. When game reports Item drag, prioritize HoveredItem + /// (HoveredAction can be stale from previous hotbar hover). Preserves full item id including HQ flag. + /// + private static unsafe bool TryGetDragPayload(out RaptureHotbarModule.HotbarSlotType slotType, out uint id) + { + slotType = RaptureHotbarModule.HotbarSlotType.Empty; + id = 0; + DragDropType gameType = DragDropType.Nothing; + try + { + var stage = AtkStage.Instance(); + if (stage != null) + { + var dm = (AtkDragDropManager*)Unsafe.AsPointer(ref stage->DragDropManager); + var dd = dm->DragDrop1; + if (dd != null) + gameType = dd->DragDropType; + } + } + catch { } + + bool isItemDrag = gameType is DragDropType.Item or DragDropType.Inventory_Item or DragDropType.RemoteInventory_Item + or DragDropType.ActionBar_Item or DragDropType.LetterEditor_Item; + + bool isMacroDrag = gameType is DragDropType.Macro or DragDropType.ActionBar_Macro; + if (isMacroDrag) + { + try + { + var stage = AtkStage.Instance(); + if (stage != null) + { + var dm = (AtkDragDropManager*)Unsafe.AsPointer(ref stage->DragDropManager); + int rawInt1 = dm->PayloadContainer.Int1; + int rawInt2 = dm->PayloadContainer.Int2; + // Int1=1 or 49 -> Individual (set 0), Int1=48 -> Shared (set 1). Int2 = 1-based macro index (1=first). + // CommandId: 1–100 Individual, 101–200 Shared. RaptureMacroModule set 0=Individual, set 1=Shared. + int set = (rawInt1 == 48) ? 1 : 0; // 48=Shared, else (1,49,...)=Individual + id = (uint)((set * 100) + rawInt2); // Int2 1-based: 1->id1, 2->id2, 3->id3 + slotType = RaptureHotbarModule.HotbarSlotType.Macro; + if (TryGetDebug() && ShouldLogMacroPayload(rawInt1, rawInt2)) + Plugin.Logger.Information($"[HSUI DragDrop DBG] Macro payload: Int1={rawInt1} Int2={rawInt2} -> set={set} idx={rawInt2} id={id}"); + return id is >= 1 and <= 200; + } + } + catch { } + return false; + } + + if (isItemDrag) + { + var hi = Plugin.GameGui?.HoveredItem ?? 0; + if (hi != 0) + { + id = (uint)(hi & 0xFFFFFFFF); + slotType = RaptureHotbarModule.HotbarSlotType.Item; + return true; + } + // HoveredItem can be 0 when game clears it; fallback: resolve from PayloadContainer (Int1=container, Int2=slot) + if (TryGetItemIdFromInventoryPayload(out id, TryGetDebug())) + { + slotType = RaptureHotbarModule.HotbarSlotType.Item; + return true; + } + return false; + } + + if (TryGetDragPayloadFromGame(out slotType, out id)) + return true; + return TryGetDragPayloadFromDalamud(out slotType, out id); + } + + /// Map game PayloadContainer Int1 (agent/UI container id) to InventoryType. + private static InventoryType ContainerIdToInventoryType(int containerId) + { + return containerId switch + { + 4 => InventoryType.EquippedItems, + 7 => InventoryType.KeyItems, + 48 => InventoryType.Inventory1, + 49 => InventoryType.Inventory2, + 50 => InventoryType.Inventory3, + 51 => InventoryType.Inventory4, + 52 => InventoryType.RetainerPage1, + 53 => InventoryType.RetainerPage2, + 54 => InventoryType.RetainerPage3, + 55 => InventoryType.RetainerPage4, + 56 => InventoryType.RetainerPage5, + 57 => InventoryType.ArmoryMainHand, + 58 => InventoryType.ArmoryHead, + 59 => InventoryType.ArmoryBody, + 60 => InventoryType.ArmoryHands, + 61 => InventoryType.ArmoryLegs, + 62 => InventoryType.ArmoryFeets, + 63 => InventoryType.ArmoryOffHand, + 64 => InventoryType.ArmoryEar, + 65 => InventoryType.ArmoryNeck, + 66 => InventoryType.ArmoryWrist, + 67 => InventoryType.ArmoryRings, + 68 => InventoryType.ArmorySoulCrystal, + 69 => InventoryType.SaddleBag1, + 70 => InventoryType.SaddleBag2, + 71 => InventoryType.PremiumSaddleBag1, + 72 => InventoryType.PremiumSaddleBag2, + _ => InventoryType.Invalid + }; + } + + /// Resolve visual (containerId, slotIndex) to real (invType, slot) for main inventory via ItemOrderModule. + private static unsafe bool TryResolveToRealSlot(int containerId, int visualSlot, out InventoryType realInvType, out int realSlot) + { + realInvType = InventoryType.Invalid; + realSlot = -1; + try + { + var iom = ItemOrderModule.Instance(); + if (iom == null) return false; + var sorter = iom->InventorySorter; + if (sorter == null) return false; + int itemsPerPage = sorter->ItemsPerPage > 0 ? sorter->ItemsPerPage : 35; + int startIndex = containerId switch { 48 => 0, 49 => itemsPerPage, 50 => itemsPerPage * 2, 51 => itemsPerPage * 3, _ => -1 }; + if (startIndex < 0) return false; + int displayIndex = startIndex + visualSlot; + if (displayIndex < 0 || displayIndex >= sorter->Items.LongCount) return false; + var entry = sorter->Items[displayIndex].Value; + if (entry == null) return false; + realInvType = InventoryType.Inventory1 + entry->Page; + realSlot = entry->Slot; + return true; + } + catch { return false; } + } + + /// Resolve item ID from inventory when HoveredItem is 0. PayloadContainer Int1=container, Int2=slot. + /// Resolves through ItemOrderModule for main inventory (sorted bags) so we get the correct item. + private static unsafe bool TryGetItemIdFromInventoryPayload(out uint itemId, bool debug = false) + { + itemId = 0; + try + { + var stage = AtkStage.Instance(); + if (stage == null) return false; + var dm = (AtkDragDropManager*)Unsafe.AsPointer(ref stage->DragDropManager); + int containerId = dm->PayloadContainer.Int1; + int slotIndex = dm->PayloadContainer.Int2; + InventoryType invType; + int slot; + if (containerId is 48 or 49 or 50 or 51 && TryResolveToRealSlot(containerId, slotIndex, out invType, out slot)) + { + if (debug && ShouldLogItemPayload(containerId, slotIndex)) + Plugin.Logger.Information($"[HSUI DragDrop DBG] Item payload: Int1={containerId} Int2={slotIndex} -> resolved to {invType} slot={slot}"); + } + else + { + invType = ContainerIdToInventoryType(containerId); + slot = slotIndex; + if (invType == InventoryType.Invalid) return false; + } + var inv = InventoryManager.Instance(); + if (inv == null) return false; + var item = inv->GetInventorySlot(invType, slot); + if (item == null || item->IsEmpty()) return false; + itemId = item->GetItemId(); + if (debug && itemId != 0 && ShouldLogItemPayload(containerId, slotIndex)) + Plugin.Logger.Information($"[HSUI DragDrop DBG] Item from inventory: Int1={containerId} Int2={slotIndex} -> {invType} slot={slot} itemId={itemId}"); + return itemId != 0; + } + catch { return false; } + } + + private static unsafe bool TryGetDragPayloadFromGame(out RaptureHotbarModule.HotbarSlotType slotType, out uint id) + { + slotType = RaptureHotbarModule.HotbarSlotType.Empty; + id = 0; + try + { + 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; + slotType = UIGlobals.GetHotbarSlotTypeFromDragDropType(dd->DragDropType); + id = (uint)dm->PayloadContainer.Int2; + return slotType != RaptureHotbarModule.HotbarSlotType.Empty; + } + catch { return false; } + } + + private static bool TryGetDragPayloadFromDalamud(out RaptureHotbarModule.HotbarSlotType slotType, out uint id) + { + slotType = RaptureHotbarModule.HotbarSlotType.Empty; + id = 0; + var ha = Plugin.GameGui?.HoveredAction; + if (ha != null && ha.ActionKind != HoverActionKind.None && ha.ActionID != 0) + { + slotType = GetHotbarSlotTypeFromHoverActionKind(ha.ActionKind); + if (slotType != RaptureHotbarModule.HotbarSlotType.Empty) + { + id = ha.ActionID; + return true; + } + } + var hi = Plugin.GameGui?.HoveredItem ?? 0; + if (hi != 0) + { + id = (uint)(hi & 0xFFFFFFFF); + slotType = RaptureHotbarModule.HotbarSlotType.Item; + return true; + } + return false; + } + + private static RaptureHotbarModule.HotbarSlotType GetHotbarSlotTypeFromHoverActionKind(HoverActionKind kind) + { + return kind switch + { + HoverActionKind.Action => RaptureHotbarModule.HotbarSlotType.Action, + HoverActionKind.CraftingAction => RaptureHotbarModule.HotbarSlotType.CraftAction, + HoverActionKind.GeneralAction => RaptureHotbarModule.HotbarSlotType.GeneralAction, + HoverActionKind.MainCommand => RaptureHotbarModule.HotbarSlotType.MainCommand, + HoverActionKind.ExtraCommand => RaptureHotbarModule.HotbarSlotType.ExtraCommand, + HoverActionKind.Companion => RaptureHotbarModule.HotbarSlotType.Companion, + HoverActionKind.PetOrder => RaptureHotbarModule.HotbarSlotType.PetAction, + HoverActionKind.BuddyAction => RaptureHotbarModule.HotbarSlotType.BuddyAction, + HoverActionKind.Mount => RaptureHotbarModule.HotbarSlotType.Mount, + HoverActionKind.BgcArmyAction => RaptureHotbarModule.HotbarSlotType.BgcArmyAction, + HoverActionKind.Perform => RaptureHotbarModule.HotbarSlotType.PerformanceInstrument, + HoverActionKind.Ornament => RaptureHotbarModule.HotbarSlotType.Ornament, + HoverActionKind.Glasses => RaptureHotbarModule.HotbarSlotType.Glasses, + HoverActionKind.MYCTemporaryItem => RaptureHotbarModule.HotbarSlotType.LostFindsItem, + HoverActionKind.QuickChat => RaptureHotbarModule.HotbarSlotType.PvPQuickChat, + HoverActionKind.ActionComboRoute => RaptureHotbarModule.HotbarSlotType.PvPCombo, + _ => RaptureHotbarModule.HotbarSlotType.Empty + }; + } + + private static unsafe uint GetIconIdForPayload(RaptureHotbarModule.HotbarSlotType slotType, uint id) + { + if (id == 0) return 0; + if (slotType == RaptureHotbarModule.HotbarSlotType.Macro) + { + var macroModule = RaptureMacroModule.Instance(); + if (macroModule != null && id is >= 1 and <= 200) + { + uint set = (id - 1) / 100; + uint index1Based = ((id - 1) % 100) + 1; // GetMacro expects 1-based index + var macro = macroModule->GetMacro(set, index1Based); + if (macro != null) + return macro->IconId; + } + return 0; + } + var module = RaptureHotbarModule.Instance(); + if (module != null && module->ModuleReady) + { + var bar = module->StandardHotbars[0]; + var slot = bar.GetHotbarSlot(0); + if (slot != null) + { + int gameIcon = slot->GetIconIdForSlot(slotType, id); + if (gameIcon > 0) return (uint)gameIcon; + } + } + try + { + switch (slotType) + { + case RaptureHotbarModule.HotbarSlotType.GeneralAction: + var gaRow = Plugin.DataManager.GetExcelSheet()?.GetRow(id); + return gaRow.HasValue ? (uint)gaRow.Value.Icon : 0; + case RaptureHotbarModule.HotbarSlotType.Action: + case RaptureHotbarModule.HotbarSlotType.CraftAction: + case RaptureHotbarModule.HotbarSlotType.PetAction: + var action = Plugin.DataManager.GetExcelSheet()?.GetRow(id); + return action.HasValue ? (uint)action.Value.Icon : 0; + case RaptureHotbarModule.HotbarSlotType.Item: + uint baseItemId = id >= 1000000 ? id - 1000000 : id; + var item = Plugin.DataManager.GetExcelSheet()?.GetRow(baseItemId); + return item.HasValue ? (uint)item.Value.Icon : 0; + case RaptureHotbarModule.HotbarSlotType.Mount: + var mount = Plugin.DataManager.GetExcelSheet()?.GetRow(id); + return mount.HasValue ? (uint)mount.Value.Icon : 0; + case RaptureHotbarModule.HotbarSlotType.Companion: + var companion = Plugin.DataManager.GetExcelSheet()?.GetRow(id); + return companion.HasValue ? (uint)companion.Value.Icon : 0; + case RaptureHotbarModule.HotbarSlotType.Emote: + var emote = Plugin.DataManager.GetExcelSheet()?.GetRow(id); + return emote.HasValue ? (uint)emote.Value.Icon : 0; + default: + return 0; + } + } + catch { return 0; } + } + + private static unsafe (string title, string text) GetSlotTooltip(ActionBarsManager.SlotInfo slot) + { + // GeneralAction uses a different ID space: GeneralAction 7 = Teleport links to Action 5. Look up via GeneralAction sheet. + if (slot.SlotType == RaptureHotbarModule.HotbarSlotType.GeneralAction) + { + if (Plugin.DataManager.GetExcelSheet()?.TryGetRow(slot.ActionId, out var gaRow) == true) + { + string name = gaRow.Name.ToString(); + string desc = ""; + try + { + string descRaw = gaRow.Description.ToDalamudString().ToString(); + if (!string.IsNullOrEmpty(descRaw)) + { + try + { + var evaluated = Plugin.SeStringEvaluator.Evaluate(gaRow.Description.AsSpan()); + desc = evaluated.ExtractText(); + if (string.IsNullOrEmpty(desc)) desc = descRaw; + } + catch { desc = descRaw; } + if (!string.IsNullOrEmpty(desc)) + desc = EncryptedStringsHelper.GetString(desc); + } + } + catch { /* ignore */ } + string statsLine = ""; + if (gaRow.Action.RowId != 0) + { + var actionRow = Plugin.DataManager.GetExcelSheet()?.GetRow(gaRow.Action.RowId); + if (actionRow.HasValue) + statsLine = TryGetActionStatsLine(actionRow.Value, includeRangeAndRadius: false); + } + if (!string.IsNullOrEmpty(statsLine)) + desc = string.IsNullOrEmpty(desc) ? statsLine : statsLine + "\n\n" + desc; + return (name, desc ?? ""); + } + } + + if (slot.SlotType == RaptureHotbarModule.HotbarSlotType.Action || + slot.SlotType == RaptureHotbarModule.HotbarSlotType.CraftAction || + slot.SlotType == RaptureHotbarModule.HotbarSlotType.PetAction) + { + var row = Plugin.DataManager.GetExcelSheet()?.GetRow(slot.ActionId); + if (row.HasValue) + { + string name = row.Value.Name.ToString(); + string desc = ""; + var descRow = Plugin.DataManager.GetExcelSheet()?.GetRow(slot.ActionId); + string descRaw = ""; + if (descRow.HasValue) + { + try + { + var descSeStr = descRow.Value.Description; + descRaw = descRow.Value.Description.ToDalamudString().ToString(); + try + { + var evaluated = Plugin.SeStringEvaluator.Evaluate(descSeStr.AsSpan()); + desc = evaluated.ExtractText(); + if (string.IsNullOrEmpty(desc)) desc = descRaw; + } + catch + { + desc = descRaw; + } + if (!string.IsNullOrEmpty(desc)) + desc = EncryptedStringsHelper.GetString(desc); + } + catch { /* ignore */ } + } + bool isCombatAction = IsCombatAction(slot.SlotType, row.Value); + int? potencyValue = isCombatAction ? TryGetPotencyValue(row.Value) : null; + string potencyLine = potencyValue.HasValue ? $"Potency: {potencyValue.Value}" : ""; + string statsLine = TryGetActionStatsLine(row.Value, includeRangeAndRadius: isCombatAction); + if (!string.IsNullOrEmpty(desc) && potencyValue.HasValue) + { + string before = desc; + desc = ReplacePotencyPlaceholderInDesc(desc, potencyValue.Value); + if (IsTooltipDebugEnabled() && before != desc) + Plugin.Logger.Information($"[HSUI Tooltip DBG] Replaced potency placeholder: actionId={slot.ActionId} value={potencyValue.Value}"); + } + else if (IsTooltipDebugEnabled() && !string.IsNullOrEmpty(desc) && desc.Contains("potency of", StringComparison.OrdinalIgnoreCase)) + Plugin.Logger.Information($"[HSUI Tooltip DBG] Potency placeholder NOT replaced: actionId={slot.ActionId} potencyValue={potencyValue?.ToString() ?? "null"} descSnippet='{(desc.Length > 60 ? desc[..60] + "..." : desc)}'"); + var statsParts = new List(); + if (!string.IsNullOrEmpty(potencyLine)) statsParts.Add(potencyLine); + if (!string.IsNullOrEmpty(statsLine)) statsParts.Add(statsLine); + if (statsParts.Count > 0) + desc = string.IsNullOrEmpty(desc) ? string.Join("\n", statsParts) : string.Join("\n", statsParts) + "\n\n" + desc; + + if (IsTooltipDebugEnabled() && ShouldLogTooltipDebug(slot.ActionId, name, desc)) + Plugin.Logger.Information($"[HSUI Tooltip DBG] GetSlotTooltip actionId={slot.ActionId} name='{name}' descRawLen={descRaw?.Length ?? 0} descLen={desc?.Length ?? 0} descRawPreview='{(descRaw?.Length > 0 ? (descRaw.Length > 50 ? descRaw[..50] + "..." : descRaw) : "(empty)")}' fullBodyPreview='{(desc != null && desc.Length > 100 ? desc[..100] + "..." : desc ?? "")}'"); + return (name, desc ?? ""); + } + } + + if (slot.SlotType == RaptureHotbarModule.HotbarSlotType.Macro) + { + try + { + var macroModule = RaptureMacroModule.Instance(); + if (macroModule != null && slot.ActionId is >= 1 and <= 200) + { + uint set = (slot.ActionId - 1) / 100; + uint index1Based = ((slot.ActionId - 1) % 100) + 1; // GetMacro expects 1-based index + var macro = macroModule->GetMacro(set, index1Based); + if (macro != null) + { + string name = macro->Name.ToString(); + if (string.IsNullOrWhiteSpace(name)) + name = (set == 0 ? "Individual" : "Shared") + " Macro " + index1Based; + var sb = new System.Text.StringBuilder(); + var lineCount = (int)macroModule->GetLineCount(macro); + for (int i = 0; i < lineCount && i < 15; i++) + { + ref var line = ref macro->Lines[i]; + if (!line.IsEmpty) + { + if (sb.Length > 0) sb.Append('\n'); + sb.Append(line.ToString()); + } + } + string desc = sb.ToString(); + return (name, desc); + } + } + } + catch { } + return ("Macro", ""); + } + + if (slot.SlotType == RaptureHotbarModule.HotbarSlotType.Item) + { + var row = Plugin.DataManager.GetExcelSheet()?.GetRow(slot.ActionId); + if (row.HasValue) + { + string name = row.Value.Name.ToString(); + try + { + string desc = row.Value.Description.ToDalamudString().ToString(); + if (!string.IsNullOrEmpty(desc)) + desc = EncryptedStringsHelper.GetString(desc); + return (name, desc); + } + catch + { + return (name, ""); + } + } + } + + return (slot.SlotType.ToString(), ""); + } + + private static int? TryGetPotencyValue(LuminaAction action) + { + try + { + var t = typeof(LuminaAction); + foreach (var name in new[] { "AttackPotency", "Effect1", "Effect2", "Effect3", "PrimaryEffect", "SecondaryEffect" }) + { + var prop = t.GetProperty(name, BindingFlags.Public | BindingFlags.Instance); + if (prop == null) continue; + var v = prop.GetValue(action); + if (v is int i && i > 0 && i <= 5000) return i; + if (v is uint u && u > 0 && u <= 5000) return (int)u; + } + // Fallback: try Omen row - potency may be stored there for weaponskills + try + { + var omenRef = action.Omen; + uint omenRowId = omenRef.RowId; + if (omenRowId != 0) + { + var omenSheet = Plugin.DataManager.GetExcelSheet(); + if (omenSheet != null && omenSheet.GetRow(omenRowId) is { } omenRow) + { + var ot = typeof(Omen); + foreach (var pname in new[] { "Effect1", "Effect2", "Effect3", "AttackPotency" }) + { + var op = ot.GetProperty(pname, BindingFlags.Public | BindingFlags.Instance); + if (op == null) continue; + var ov = op.GetValue(omenRow); + if (ov is int oi && oi >= 50 && oi <= 2000) return oi; + if (ov is uint ou && ou >= 50 && ou <= 2000) return (int)ou; + } + } + } + } + catch { /* ignore */ } + // Last resort: scan Action int/uint properties + foreach (var prop in t.GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (prop.PropertyType != typeof(int) && prop.PropertyType != typeof(uint)) continue; + var name = prop.Name; + if (name.Contains("Id") || name.Contains("Index") || name.Contains("Time") || + name.Contains("Range") || name.Contains("Radius") || name.Contains("Cost") || + name.Contains("Level") || name.Contains("Category") || name.Contains("Cast") || + name.Contains("Recast") || name.Contains("Omen")) + continue; + var v = prop.GetValue(action); + if (v is int i && i >= 50 && i <= 2000) return i; + if (v is uint u && u >= 50 && u <= 2000) return (int)u; + } + } + catch { /* ignore */ } + return null; + } + + private static string ReplacePotencyPlaceholderInDesc(string desc, int potencyValue) + { + if (string.IsNullOrEmpty(desc)) return desc; + string replaced = desc; + // Game uses placeholder that can render as ".", "?", etc. Try common patterns + replaced = System.Text.RegularExpressions.Regex.Replace(replaced, + @"(potency of\s+)[^0-9]{1,4}", + $"$1{potencyValue}", + System.Text.RegularExpressions.RegexOptions.IgnoreCase); + if (replaced == desc && desc.Contains("potency of .", StringComparison.OrdinalIgnoreCase)) + { + int idx = desc.IndexOf("potency of .", StringComparison.OrdinalIgnoreCase); + if (idx >= 0) + replaced = desc[..(idx + 10)] + potencyValue + desc[(idx + 11)..]; + } + return replaced; + } + + /// Returns true if this action should show combat stats (potency, range, radius). False for Teleport, Return, Sprint, etc. + private static bool IsCombatAction(RaptureHotbarModule.HotbarSlotType slotType, LuminaAction action) + { + // GeneralAction: Teleport, Return, Sprint, Limit Break - never show combat stats + if (slotType == RaptureHotbarModule.HotbarSlotType.GeneralAction) + return false; + // CraftAction: crafting actions have cast/recast but no combat range/radius + if (slotType == RaptureHotbarModule.HotbarSlotType.CraftAction) + return false; + // PetAction: usually combat, but some pets have non-combat skills - keep combat stats for now + // Action: check ActionCategory - category 4 (General) and similar are non-combat + try + { + var catRef = action.ActionCategory; + uint rowId = catRef.RowId; + if (rowId is 4 or 5) return false; // General, Other + } + catch { /* ignore */ } + return true; + } + + private static string TryGetActionStatsLine(LuminaAction action, bool includeRangeAndRadius = true) + { + try + { + var parts = new List(); + float castSec = action.Cast100ms / 100f; + float recastSec = action.Recast100ms / 100f; + if (castSec > 0) + parts.Add($"Cast: {castSec:F1}s"); + else + parts.Add("Cast: Instant"); + if (recastSec > 0) + parts.Add($"Recast: {recastSec:F1}s"); + if (includeRangeAndRadius) + { + int range = action.Range; + int effectRange = action.EffectRange; + if (range > 0) + parts.Add($"Range: {range}y"); + if (effectRange > 0) + parts.Add($"Radius: {effectRange}y"); + } + return parts.Count > 0 ? string.Join(" | ", parts) : ""; + } + catch { return ""; } + } + } +} diff --git a/Interface/GeneralElements/BarTexturesConfig.cs b/Interface/GeneralElements/BarTexturesConfig.cs new file mode 100644 index 0000000..5623115 --- /dev/null +++ b/Interface/GeneralElements/BarTexturesConfig.cs @@ -0,0 +1,159 @@ +using Dalamud.Interface; +using Dalamud.Interface.ImGuiFileDialog; +using Dalamud.Memory.Exceptions; +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Enums; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using Dalamud.Bindings.ImGui; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace HSUI.Interface.GeneralElements +{ + [Disableable(false)] + [Section("Customization")] + [SubSection("Bar Textures", 0)] + public class BarTexturesConfig : PluginConfigObject + { + public new static BarTexturesConfig DefaultConfig() { return new BarTexturesConfig(); } + + public string BarTexturesPath = "C:\\"; + + [JsonIgnore] public string ValidatedBarTexturesPath => ValidatePath(BarTexturesPath); + + [JsonIgnore] private int _inputBarTexture = 0; + [JsonIgnore] private int _drawModeIndex = 0; + [JsonIgnore] private Vector4 _color = new Vector4(229 / 255f, 57 / 255f, 57 / 255f, 1); + [JsonIgnore] private PluginConfigColor _pluginConfigColor = PluginConfigColor.FromHex(0xFFE53939); + [JsonIgnore] private FileDialogManager _fileDialogManager = new FileDialogManager(); + [JsonIgnore] private bool _applying = false; + + private string ValidatePath(string path) + { + if (path.EndsWith("\\") || path.EndsWith("/")) + { + return path; + } + + return path + "\\"; + } + + private void SelectFolder() + { + Action callback = (finished, path) => + { + if (finished && path.Length > 0) + { + BarTexturesPath = path; + BarTexturesManager.Instance?.ReloadTextures(); + } + }; + + _fileDialogManager.OpenFolderDialog("Select Bar Textures Folder", callback); + } + + [ManualDraw] + public bool Draw(ref bool changed) + { + if (BarTexturesManager.Instance == null) { return false; } + + string[] textureNames = BarTexturesManager.Instance.BarTextureNames.ToArray(); + string[] drawModes = new string[] { "Stretch", "Repeat Horizontal", "Repeat Vertical", "Repeat" }; + + if (ImGui.BeginChild("Bar Textures", new Vector2(800, 400), false, ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse)) + { + ImGuiHelper.NewLineAndTab(); + ImGui.Text("Custom Bar Textures path"); + + ImGuiHelper.Tab(); + if (ImGui.InputText("", ref BarTexturesPath, 200, ImGuiInputTextFlags.EnterReturnsTrue)) + { + changed = true; + BarTexturesManager.Instance?.ReloadTextures(); + } + + ImGui.SameLine(); + ImGui.PushFont(UiBuilder.IconFont); + if (ImGui.Button(FontAwesomeIcon.Folder.ToIconString(), new Vector2(0, 0))) + { + SelectFolder(); + } + ImGui.PopFont(); + + ImGuiHelper.NewLineAndTab(); + ImGui.Text("Preview"); + ImGuiHelper.Tab(); + ImGui.Combo("Bar Texture ##bar texture", ref _inputBarTexture, textureNames); + + ImGuiHelper.Tab(); + ImGui.Combo("Draw Mode", ref _drawModeIndex, drawModes); + + ImGuiHelper.Tab(); + if (ImGui.ColorEdit4("Color", ref _color)) + { + _pluginConfigColor = new PluginConfigColor(_color); + } + + if (textureNames.Length > _inputBarTexture) + { + // draw preview + ImGui.NewLine(); + ImGuiHelper.NewLineAndTab(); + Vector2 pos = ImGui.GetWindowPos() + ImGui.GetCursorPos(); + Vector2 size = new Vector2(512, 64); + ImDrawListPtr drawList = ImGui.GetWindowDrawList(); + + DrawHelper.DrawBarTexture( + pos, + size, + _pluginConfigColor, + textureNames[_inputBarTexture], + (BarTextureDrawMode)_drawModeIndex, + drawList + ); + + ImGuiHelper.DrawSpacing(3); + ImGuiHelper.NewLineAndTab(); + if (ImGui.Button("Apply to all bars", new Vector2(200, 30))) + { + _applying = true; + } + } + } + + ImGui.EndChild(); + + _fileDialogManager.Draw(); + + if (_applying) + { + string[] lines = new string[] { "This will replace the Bar Texture", "and Draw Mode for ALL bars!", "THIS CAN'T BE UNDONE!", "Are you sure?" }; + var (didConfirm, didClose) = ImGuiHelper.DrawConfirmationModal("Apply to ALL bars?", lines); + + if (didConfirm) + { + List barConfigs = ConfigurationManager.Instance.GetObjects(); + foreach (BarConfig barConfig in barConfigs) + { + barConfig.BarTextureName = textureNames[_inputBarTexture]; + barConfig.BarTextureDrawMode = (BarTextureDrawMode)_drawModeIndex; + } + + changed = true; + } + + if (didConfirm || didClose) + { + _applying = false; + } + } + + return false; + } + } +} diff --git a/Interface/GeneralElements/CastbarConfig.cs b/Interface/GeneralElements/CastbarConfig.cs new file mode 100644 index 0000000..21fe472 --- /dev/null +++ b/Interface/GeneralElements/CastbarConfig.cs @@ -0,0 +1,258 @@ +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Enums; +using HSUI.Interface.Bars; +using System; +using System.Numerics; + +namespace HSUI.Interface.GeneralElements +{ + [Section("Castbars")] + [SubSection("Player", 0)] + public class PlayerCastbarConfig : UnitFrameCastbarConfig + { + [Checkbox("Use Job Color", spacing = true)] + [Order(19)] + public bool UseJobColor = false; + + [Checkbox("Slide Cast", spacing = true)] + [Order(60)] + public bool ShowSlideCast = true; + + [DragInt("Time (milliseconds)", min = 0, max = 10000)] + [Order(61, collapseWith = nameof(ShowSlideCast))] + public int SlideCastTime = 500; + + [ColorEdit4("Color ##SlidecastColor")] + [Order(62, collapseWith = nameof(ShowSlideCast))] + public PluginConfigColor SlideCastColor = new PluginConfigColor(new(190f / 255f, 28f / 255f, 57f / 255f, 100f / 100f)); + + public PlayerCastbarConfig(Vector2 position, Vector2 size, LabelConfig castNameConfig, NumericLabelConfig castTimeConfig) + : base(position, size, castNameConfig, castTimeConfig) + { + + } + + public new static PlayerCastbarConfig DefaultConfig() + { + var size = new Vector2(254, 24); + var pos = new Vector2(0, HUDConstants.PlayerCastbarY); + + var castNameConfig = new LabelConfig(new Vector2(5, 0), "", DrawAnchor.Left, DrawAnchor.Left); + var castTimeConfig = new NumericLabelConfig(new Vector2(-5, 0), "", DrawAnchor.Right, DrawAnchor.Right); + castTimeConfig.NumberFormat = 1; + + return new PlayerCastbarConfig(pos, size, castNameConfig, castTimeConfig); + } + } + + [Section("Castbars")] + [SubSection("Target", 0)] + public class TargetCastbarConfig : UnitFrameCastbarConfig + { + [Checkbox("Interruptable Color", spacing = true)] + [Order(50)] + public bool ShowInterruptableColor = true; + + [ColorEdit4("Interruptable")] + [Order(51, collapseWith = nameof(ShowInterruptableColor))] + public PluginConfigColor InterruptableColor = new PluginConfigColor(new(255f / 255f, 87f / 255f, 113f / 255f, 100f / 100f)); + + [Checkbox("Damage Type Colors", spacing = true)] + [Order(60)] + public bool UseColorForDamageTypes = true; + + [ColorEdit4("Physical")] + [Order(61, collapseWith = nameof(UseColorForDamageTypes))] + public PluginConfigColor PhysicalDamageColor = new PluginConfigColor(new(190f / 255f, 28f / 255f, 57f / 255f, 100f / 100f)); + + [ColorEdit4("Magical")] + [Order(62, collapseWith = nameof(UseColorForDamageTypes))] + public PluginConfigColor MagicalDamageColor = new PluginConfigColor(new(0f / 255f, 72f / 255f, 179f / 255f, 100f / 100f)); + + [ColorEdit4("Darkness")] + [Order(63, collapseWith = nameof(UseColorForDamageTypes))] + public PluginConfigColor DarknessDamageColor = new PluginConfigColor(new(188f / 255f, 19f / 255f, 254f / 255f, 100f / 100f)); + + public TargetCastbarConfig(Vector2 position, Vector2 size, LabelConfig castNameConfig, NumericLabelConfig castTimeConfig) + : base(position, size, castNameConfig, castTimeConfig) + { + + } + public new static TargetCastbarConfig DefaultConfig() + { + var size = new Vector2(254, 24); + var pos = new Vector2(0, HUDConstants.BaseHUDOffsetY / 2f - size.Y / 2); + + var castNameConfig = new LabelConfig(new Vector2(5, 0), "", DrawAnchor.Left, DrawAnchor.Left); + var castTimeConfig = new NumericLabelConfig(new Vector2(-5, 0), "", DrawAnchor.Right, DrawAnchor.Right); + castTimeConfig.NumberFormat = 1; + + return new TargetCastbarConfig(pos, size, castNameConfig, castTimeConfig); + } + } + + [Section("Castbars")] + [SubSection("Target of Target", 0)] + public class TargetOfTargetCastbarConfig : TargetCastbarConfig + { + public TargetOfTargetCastbarConfig(Vector2 position, Vector2 size, LabelConfig castNameConfig, NumericLabelConfig castTimeConfig) + : base(position, size, castNameConfig, castTimeConfig) + { + + } + public new static TargetOfTargetCastbarConfig DefaultConfig() + { + var size = new Vector2(120, 24); + var pos = new Vector2(0, -1); + + var castNameConfig = new LabelConfig(new Vector2(0, 0), "", DrawAnchor.Center, DrawAnchor.Center); + var castTimeConfig = new NumericLabelConfig(new Vector2(-5, 0), "", DrawAnchor.Right, DrawAnchor.Right); + castTimeConfig.Enabled = false; + castTimeConfig.NumberFormat = 1; + + var config = new TargetOfTargetCastbarConfig(pos, size, castNameConfig, castTimeConfig); + config.Anchor = DrawAnchor.Top; + config.AnchorToUnitFrame = true; + config.ShowIcon = false; + + return config; + } + } + + [Section("Castbars")] + [SubSection("Focus Target", 0)] + public class FocusTargetCastbarConfig : TargetCastbarConfig + { + public FocusTargetCastbarConfig(Vector2 position, Vector2 size, LabelConfig castNameConfig, NumericLabelConfig castTimeConfig) + : base(position, size, castNameConfig, castTimeConfig) + { + + } + public new static FocusTargetCastbarConfig DefaultConfig() + { + var size = new Vector2(120, 24); + var pos = new Vector2(0, -1); + + var castNameConfig = new LabelConfig(new Vector2(0, 0), "", DrawAnchor.Center, DrawAnchor.Center); + var castTimeConfig = new NumericLabelConfig(new Vector2(-5, 0), "", DrawAnchor.Right, DrawAnchor.Right); + castTimeConfig.Enabled = false; + castTimeConfig.NumberFormat = 1; + + var config = new FocusTargetCastbarConfig(pos, size, castNameConfig, castTimeConfig); + config.Anchor = DrawAnchor.Top; + config.AnchorToUnitFrame = true; + config.ShowIcon = false; + + return config; + } + } + + public abstract class UnitFrameCastbarConfig : CastbarConfig + { + [Checkbox("Anchor to Unit Frame")] + [Order(16)] + public bool AnchorToUnitFrame = false; + + [Anchor("Unit Frame Anchor")] + [Order(17, collapseWith = nameof(AnchorToUnitFrame))] + public DrawAnchor UnitFrameAnchor = DrawAnchor.Bottom; + + public UnitFrameCastbarConfig(Vector2 position, Vector2 size, LabelConfig castNameConfig, NumericLabelConfig castTimeConfig) + : base(position, size, castNameConfig, castTimeConfig) + { + + } + } + + [DisableParentSettings("HideWhenInactive")] + public abstract class CastbarConfig : BarConfig + { + [Checkbox("Preview")] + [Order(3)] + public bool Preview = false; + + [Checkbox("Show Ability Icon")] + [Order(4)] + public bool ShowIcon = true; + + [Checkbox("Reverse Fill Background Color")] + [Order(5)] + public bool UseReverseFill = false; + + [Checkbox("Show Current Cast Time + Max Cast Time")] + [Order(6)] + public bool ShowMaxCastTime = false; + + [Checkbox("Truncate Cast Name", help = "This will automatically truncate the cast name if it's too long and won't fit inside the bar.")] + [Order(7)] + public bool TruncateCastName = false; + + [Checkbox("Separate Icon", spacing = true)] + [Order(100)] + public bool SeparateIcon = false; + + [DragInt2("Custom Icon Position", min = -500, max = 500)] + [Order(101, collapseWith = nameof(SeparateIcon))] + public Vector2 CustomIconPosition = Vector2.Zero; + + [DragInt2("Custom Icon Size", min = 1, max = 500)] + [Order(101, collapseWith = nameof(SeparateIcon))] + public Vector2 CustomIconSize = new Vector2(40); + + [NestedConfig("Cast Name", 500)] + public LabelConfig CastNameLabel; + + [NestedConfig("Cast Time", 505)] + public NumericLabelConfig CastTimeLabel; + + [ColorEdit4("Color" + "##ReverseFill")] + [Order(515, collapseWith = nameof(UseReverseFill))] + public PluginConfigColor ReverseFillColor = new(new Vector4(255f / 255f, 0f / 255f, 0f / 255f, 100f / 100f)); + + public CastbarConfig(Vector2 position, Vector2 size, LabelConfig castNameConfig, NumericLabelConfig castTimeConfig) + : base(position, size, new PluginConfigColor(new(0f / 255f, 162f / 255f, 252f / 255f, 100f / 100f)), BarDirection.Right) + { + CastNameLabel = castNameConfig; + CastTimeLabel = castTimeConfig; + + Strata = StrataLevel.MID; + } + } + + public class CastbarConfigConverter : PluginConfigObjectConverter + { + public CastbarConfigConverter() + { + SameClassFieldConverter name = new SameClassFieldConverter( + "CastNameLabel", + new LabelConfig(Vector2.Zero, "", DrawAnchor.Center, DrawAnchor.Center) + ); + + NewClassFieldConverter time = new NewClassFieldConverter( + "CastTimeLabel", + new NumericLabelConfig(new Vector2(-5, 0), "", DrawAnchor.Right, DrawAnchor.Right), + (oldValue) => + { + NumericLabelConfig label = new NumericLabelConfig(oldValue.Position, "", oldValue.FrameAnchor, oldValue.TextAnchor); + label.Enabled = oldValue.Enabled; + label.FontID = oldValue.FontID; + label.NumberFormat = 1; + label.Color = oldValue.Color; + label.OutlineColor = oldValue.OutlineColor; + label.ShadowConfig = oldValue.ShadowConfig; + label.UseJobColor = oldValue.UseJobColor; + + return label; + }); + + FieldConvertersMap.Add("CastNameConfig", name); + FieldConvertersMap.Add("CastTimeConfig", time); + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(CastbarConfig); + } + } +} diff --git a/Interface/GeneralElements/CastbarHud.cs b/Interface/GeneralElements/CastbarHud.cs new file mode 100644 index 0000000..8efcc0a --- /dev/null +++ b/Interface/GeneralElements/CastbarHud.cs @@ -0,0 +1,509 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Interface.Textures.TextureWraps; +using Dalamud.Utility; +using HSUI.Config; +using HSUI.Enums; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using HSUI.Interface.EnemyList; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Component.GUI; +using System; +using System.Collections.Generic; +using System.Numerics; +using LuminaAction = Lumina.Excel.Sheets.Action; +using StructsBattleChara = FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara; + +namespace HSUI.Interface.GeneralElements +{ + public class CastbarHud : ParentAnchoredDraggableHudElement, IHudElementWithActor, IHudElementWithAnchorableParent, IHudElementWithPreview + { + private CastbarConfig Config => (CastbarConfig)_config; + private readonly LabelHud _castNameLabel; + private readonly LabelHud _castTimeLabel; + + protected LastUsedCast? LastUsedCast; + + public IGameObject? Actor { get; set; } + + protected override bool AnchorToParent => Config is UnitFrameCastbarConfig { AnchorToUnitFrame: true }; + protected override DrawAnchor ParentAnchor => Config is UnitFrameCastbarConfig config ? config.UnitFrameAnchor : DrawAnchor.Center; + + public CastbarHud(CastbarConfig config, string? displayName = null) : base(config, displayName) + { + _castNameLabel = new LabelHud(config.CastNameLabel); + _castTimeLabel = new LabelHud(config.CastTimeLabel); + } + + public void StopPreview() + { + Config.Preview = false; + } + + protected override (List, List) ChildrenPositionsAndSizes() => (new List { Config.Position }, new List { Config.Size }); + + public override void DrawChildren(Vector2 origin) + { + if (!Config.Enabled) + { + return; + } + + if (!Config.Preview && + (Actor == null || Actor is not ICharacter || Actor.ObjectKind != ObjectKind.Player && Actor.ObjectKind != ObjectKind.BattleNpc)) + { + return; + } + + UpdateCurrentCast(out float currentCastTime, out float totalCastTime); + if (totalCastTime == 0 || currentCastTime >= totalCastTime) + { + return; + } + + if (!ShouldShow() && !Config.Preview) + { + return; + } + + Vector2 size = GetSize(); + IDalamudTextureWrap? iconTexture = LastUsedCast?.GetIconTexture(); + bool validIcon = Config.Preview ? true : iconTexture is not null; + Vector2 iconSize = Config.ShowIcon && validIcon && !Config.SeparateIcon ? new Vector2(size.Y, size.Y) : Vector2.Zero; + + PluginConfigColor fillColor = GetColor(); + Rect background = new(Config.Position, size, Config.BackgroundColor); + Rect progress = BarUtilities.GetFillRect(Config.Position, size, Config.FillDirection, fillColor, currentCastTime, totalCastTime); + + BarHud bar = new(Config, Actor);/**/ + bar.SetBackground(background); + + if (Config.UseReverseFill) + { + Vector2 reverseFillSize = size - BarUtilities.GetFillDirectionOffset(progress.Size, Config.FillDirection); + Vector2 reverseFillPos = Config.FillDirection.IsInverted() + ? Config.Position + : Config.Position + BarUtilities.GetFillDirectionOffset(progress.Size, Config.FillDirection); + + PluginConfigColor reverseFillColor = Config.ReverseFillColor; + bar.AddForegrounds(new Rect(reverseFillPos, reverseFillSize, reverseFillColor)); + } + + AddExtras(bar, totalCastTime, iconTexture); + + bar.AddForegrounds(progress); + + Vector2 pos = origin + ParentPos(); + AddDrawActions(bar.GetDrawActions(pos, Config.StrataLevel)); + + // icon + Vector2 startPos = Config.Position + Utils.GetAnchoredPosition(pos, size, Config.Anchor); + if (Config.ShowIcon && validIcon) + { + Vector2 finalIconPos = Config.SeparateIcon ? startPos + Config.CustomIconPosition : startPos; + Vector2 finalIconSize = Config.SeparateIcon ? Config.CustomIconSize : iconSize; + + AddDrawAction(Config.StrataLevel, () => + { + DrawHelper.DrawInWindow(ID + "_icon", finalIconPos, finalIconSize, false, (drawList) => + { + ImGui.SetCursorPos(finalIconPos); + + IDalamudTextureWrap? texture = Config.Preview ? TexturesHelper.GetTexture(3577) : iconTexture; + if (texture != null) + { + ImGui.Image(texture.Handle, finalIconSize); + } + + if (Config.DrawBorder) + { + drawList.AddRect(finalIconPos, finalIconPos + finalIconSize, Config.BorderColor.Base, 0, ImDrawFlags.None, Config.BorderThickness); + } + }); + }); + } + + // cast time + bool isTimeLeftAnchored = Config.CastTimeLabel.TextAnchor is DrawAnchor.Left or DrawAnchor.TopLeft or DrawAnchor.BottomLeft; + Vector2 timePos = Config.ShowIcon && isTimeLeftAnchored ? startPos + new Vector2(iconSize.X, 0) : startPos; + float value = Config.Preview ? 0.5f : totalCastTime - currentCastTime; + + if (Config.ShowMaxCastTime) + { + string format = Config.CastTimeLabel.NumberFormat.ToString(); + Config.CastTimeLabel.SetText( + value.ToString("N" + format, ConfigurationManager.Instance.ActiveCultreInfo) + + " / " + + totalCastTime.ToString("N" + format, ConfigurationManager.Instance.ActiveCultreInfo) + ); + } + else + { + Config.CastTimeLabel.SetValue(value); + } + + AddDrawAction(Config.CastTimeLabel.StrataLevel, () => + { + _castTimeLabel.Draw(timePos, size, Actor); + }); + + // cast name + bool isNameLeftAnchored = Config.CastNameLabel.TextAnchor is DrawAnchor.Left or DrawAnchor.TopLeft or DrawAnchor.BottomLeft; + Vector2 namePos = Config.ShowIcon && isNameLeftAnchored ? startPos + new Vector2(iconSize.X, 0) : startPos; + + string original = CustomCastName() ?? (LastUsedCast?.ActionText ?? ""); + string castName = EncryptedStringsHelper.GetString(original).CheckForUpperCase() ?? original; + + if (Config.TruncateCastName) + { + castName = TruncatedCastName(castName) ?? castName; + } + + Config.CastNameLabel.SetText(Config.Preview ? "Cast Name" : castName); + + AddDrawAction(Config.CastNameLabel.StrataLevel, () => + { + _castNameLabel.Draw(namePos, size, Actor); + }); + } + + private unsafe void UpdateCurrentCast(out float currentCastTime, out float totalCastTime) + { + if (Config.Preview || Actor is not IBattleChara battleChara) + { + currentCastTime = Config.Preview ? 0.5f : 0f; + totalCastTime = 1f; + return; + } + + float current = 0; + float total = 0; + + try + { + current = battleChara.CurrentCastTime; + StructsBattleChara* chara = (StructsBattleChara*)battleChara.Address; + CastInfo* castInfo = chara->GetCastInfo(); + + if (castInfo != null) + { + total = castInfo->TotalCastTime; + } + } + catch + { + currentCastTime = 0; + totalCastTime = 0; + return; + } + + if (!Utils.IsActorCasting(battleChara) && current <= 0) + { + currentCastTime = 0; + totalCastTime = 0; + return; + } + + currentCastTime = current; + totalCastTime = total; + + uint currentCastId = battleChara.CastActionId; + ActionType currentCastType = (ActionType)battleChara.CastActionType; + + if (LastUsedCast == null || LastUsedCast.CastId != currentCastId || LastUsedCast.ActionType != currentCastType) + { + LastUsedCast = new LastUsedCast(currentCastId, currentCastType, battleChara.IsCastInterruptible); + } + } + + private string? TruncatedCastName(string text) + { + if (text.Length <= 5) + { + return null; + } + + LabelConfig castNamelabel = Config.CastNameLabel; + LabelConfig castTimeLabel = Config.CastTimeLabel; + + + Vector2 size; + + using (FontsManager.Instance.PushFont(castNamelabel.FontID)) + { + size = ImGui.CalcTextSize(text) * castNamelabel.GetFontScale(); + } + + float maxWidth = Config.Size.X; + + if (!Config.SeparateIcon) + { + maxWidth -= Config.Size.Y; + } + + if (Config.CastTimeLabel.Enabled) + { + using (FontsManager.Instance.PushFont(Config.CastTimeLabel.FontID)) + { + maxWidth -= (ImGui.CalcTextSize("XX.X") * castTimeLabel.GetFontScale()).X; + } + } + + if (size.X > maxWidth) + { + return TruncatedCastName(text.Substring(0, text.Length - 5) + "..."); + } + + return text; + } + + public virtual void AddExtras(BarHud bar, float totalCastTime, IDalamudTextureWrap? iconTexture) + { + // override + } + + public virtual string? CustomCastName() + { + // override + return null; + } + + public virtual PluginConfigColor GetColor() => Config.FillColor; + public virtual Vector2 GetSize() => Config.Size; + + public virtual bool ShouldShow() => true; + } + + public class PlayerCastbarHud : CastbarHud + { + private PlayerCastbarConfig Config => (PlayerCastbarConfig)_config; + + public PlayerCastbarHud(PlayerCastbarConfig config, string displayName) : base(config, displayName) + { + + } + + public override unsafe string? CustomCastName() + { + AddonCastBar* castBar = (AddonCastBar*)Plugin.GameGui.GetAddonByName("_CastBar", 1).Address; + if (castBar == null) { return null; } + + AtkTextNode* node = castBar->GetTextNodeById(4); + if (node == null) { return null; } + + return node->GetText().ExtractText(); + } + + public override void AddExtras(BarHud bar, float totalCastTime, IDalamudTextureWrap? iconTexture) + { + if (!Config.ShowSlideCast || Config.SlideCastTime <= 0 || Config.Preview) + { + return; + } + + Rect slideCast; + + if (Config.FillDirection.IsHorizontal()) + { + float slideCastWidth = Math.Min(Config.Size.X, Config.SlideCastTime / 1000f * Config.Size.X / totalCastTime); + Vector2 size = new(slideCastWidth, Config.Size.Y); + slideCast = new(Config.Position + Config.Size - size, size, Config.SlideCastColor); + + if (Config.FillDirection is BarDirection.Left) + { + bool validIcon = iconTexture is not null; + Vector2 iconSize = Config.ShowIcon && validIcon ? new Vector2(Config.Size.Y, Config.Size.Y) : Vector2.Zero; + slideCast = Config.ShowIcon ? new Rect(Config.Position, size + new Vector2(iconSize.X, 0), Config.SlideCastColor) : new Rect(Config.Position, size, Config.SlideCastColor); + } + } + else + { + float slideCastHeight = Math.Min(Config.Size.Y, Config.SlideCastTime / 1000f * Config.Size.Y / totalCastTime); + Vector2 size = new(Config.Size.X, slideCastHeight); + slideCast = new(Config.Position + Config.Size - size, size, Config.SlideCastColor); + + if (Config.FillDirection is BarDirection.Up) + { + slideCast = new(Config.Position, size, Config.SlideCastColor); + } + } + + bar.AddForegrounds(slideCast); + } + + public override PluginConfigColor GetColor() + { + if (!Config.UseJobColor || Actor is not ICharacter) + { + return Config.FillColor; + } + + ICharacter? chara = (ICharacter)Actor; + PluginConfigColor? color = GlobalColors.Instance.ColorForJobId(chara.ClassJob.RowId); + return color ?? Config.FillColor; + } + } + + public class TargetCastbarHud : CastbarHud + { + private TargetCastbarConfig Config => (TargetCastbarConfig)_config; + + public TargetCastbarHud(TargetCastbarConfig config, string? displayName = null) : base(config, displayName) + { + + } + + public override PluginConfigColor GetColor() + { + if (Config.ShowInterruptableColor && LastUsedCast?.Interruptible == true) + { + return Config.InterruptableColor; + } + + if (!Config.UseColorForDamageTypes) + { + return Config.FillColor; + } + + if (LastUsedCast != null) + { + switch (LastUsedCast.DamageType) + { + case DamageType.Physical: + case DamageType.Blunt: + case DamageType.Slashing: + case DamageType.Piercing: + return Config.PhysicalDamageColor; + + case DamageType.Magic: + return Config.MagicalDamageColor; + + case DamageType.Darkness: + return Config.DarknessDamageColor; + } + } + + return Config.FillColor; + } + + public override unsafe bool ShouldShow() + { + bool? targetCasting = Utils.IsTargetCasting(); + if (targetCasting.HasValue) + { + return targetCasting.Value; + } + + return true; + } + } + + public class FocusTargetCastbarHud : TargetCastbarHud + { + private TargetCastbarConfig Config => (TargetCastbarConfig)_config; + + public FocusTargetCastbarHud(TargetCastbarConfig config, string? displayName = null) : base(config, displayName) + { + + } + + public override unsafe bool ShouldShow() + { + bool? focusTargetCasting = Utils.IsFocusTargetCasting(); + if (focusTargetCasting.HasValue) + { + return focusTargetCasting.Value; + } + + return true; + } + } + + public class TargetOfTargetCastbarHud : TargetCastbarHud + { + private TargetCastbarConfig Config => (TargetCastbarConfig)_config; + + public TargetOfTargetCastbarHud(TargetCastbarConfig config, string? displayName = null) : base(config, displayName) + { + + } + + public override unsafe bool ShouldShow() + { + IGameObject? target = Plugin.TargetManager.SoftTarget ?? Plugin.TargetManager.Target; + if (Actor == target) + { + bool? targetCasting = Utils.IsTargetCasting(); + if (targetCasting.HasValue) + { + return targetCasting.Value; + } + } + + IGameObject? focusTarget = Plugin.TargetManager.FocusTarget; + if (Actor == focusTarget) + { + bool? focusTargetCasting = Utils.IsFocusTargetCasting(); + if (focusTargetCasting.HasValue) + { + return focusTargetCasting.Value; + } + } + + return true; + } + } + + public class EnemyListCastbarHud : TargetCastbarHud + { + private EnemyListCastbarConfig Config => (EnemyListCastbarConfig)_config; + + public int EnemyListIndex = 0; + + public EnemyListCastbarHud(EnemyListCastbarConfig config, string? displayName = null) : base(config, displayName) + { + + } + + public override unsafe bool ShouldShow() + { + bool? casting = Utils.IsEnemyInListCasting(EnemyListIndex); + if (casting.HasValue) + { + return casting.Value; + } + + return true; + } + } + + public class NameplateCastbarHud : TargetOfTargetCastbarHud + { + private NameplateCastbarConfig Config => (NameplateCastbarConfig)_config; + + private Vector2 _customSize = new Vector2(0); + public Vector2 ParentSize { get; set; } = new Vector2(0); + + public NameplateCastbarHud(NameplateCastbarConfig config, string? displayName = null) : base(config, displayName) + { + _customSize = Config.Size; + } + + public override void DrawChildren(Vector2 origin) + { + // calculate size + float x = Config.MatchWidth ? ParentSize.X : Config.Size.X; + float y = Config.MatchHeight ? ParentSize.Y : Config.Size.Y; + _customSize = new Vector2(x, y); + + // draw + base.DrawChildren(origin); + } + + public override Vector2 GetSize() => _customSize; + } +} diff --git a/Interface/GeneralElements/ExperienceBarConfig.cs b/Interface/GeneralElements/ExperienceBarConfig.cs new file mode 100644 index 0000000..cf2c2b2 --- /dev/null +++ b/Interface/GeneralElements/ExperienceBarConfig.cs @@ -0,0 +1,57 @@ +using Dalamud.Interface; +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Enums; +using HSUI.Interface.Bars; +using Dalamud.Bindings.ImGui; +using System.Numerics; + +namespace HSUI.Interface.GeneralElements +{ + [Section("Other Elements")] + [SubSection("Experience Bar", 0)] + public class ExperienceBarConfig : BarConfig + { + [Checkbox("Hide When Downsynced")] + [Order(44, collapseWith = nameof(HideWhenInactive))] + public bool HideWhenDownsynced = false; + + [Checkbox("Use Job Color")] + [Order(45)] + public bool UseJobColor = false; + + [Checkbox("Show Rested Exp")] + [Order(50)] + public bool ShowRestedExp = true; + + [ColorEdit4("Rested Exp Color")] + [Order(55, collapseWith = nameof(ShowRestedExp))] + public PluginConfigColor RestedExpColor = new PluginConfigColor(new Vector4(110f / 255f, 197f / 255f, 207f / 255f, 50f / 100f)); + + [NestedConfig("Left Text", 60)] + public EditableLabelConfig LeftLabel; + + [NestedConfig("Right Text", 61)] + public EditableLabelConfig RightLabel; + + [NestedConfig("Sanctuary Icon", 62)] + public IconLabelConfig SanctuaryLabel = new IconLabelConfig(new Vector2(5, 0), FontAwesomeIcon.Moon, DrawAnchor.Right, DrawAnchor.Left); + + [NestedConfig("Visibility", 70)] + public VisibilityConfig VisibilityConfig = new VisibilityConfig(); + + public ExperienceBarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) : base(position, size, fillColor) + { + LeftLabel = new EditableLabelConfig(new Vector2(5, 0), "[job] Lv[level] EXP [exp:current-short]/[exp:required-short]", DrawAnchor.BottomLeft, DrawAnchor.TopLeft); + RightLabel = new EditableLabelConfig(new Vector2(-5, 0), "([exp:percent]%)", DrawAnchor.BottomRight, DrawAnchor.TopRight); + } + + public new static ExperienceBarConfig DefaultConfig() + { + return new ExperienceBarConfig( + new Vector2(0, -ImGui.GetMainViewport().Size.Y * 0.45f), + new Vector2(860, 10), + new PluginConfigColor(new Vector4(211f / 255f, 166f / 255f, 79f / 255f, 100f / 100f))); + } + } +} diff --git a/Interface/GeneralElements/ExperienceBarHud.cs b/Interface/GeneralElements/ExperienceBarHud.cs new file mode 100644 index 0000000..500764a --- /dev/null +++ b/Interface/GeneralElements/ExperienceBarHud.cs @@ -0,0 +1,84 @@ +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Interface; +using HSUI.Config; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using FFXIVClientStructs.FFXIV.Client.UI; +using System.Collections.Generic; +using System.Numerics; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace HSUI.Interface.GeneralElements +{ + public class ExperienceBarHud : DraggableHudElement, IHudElementWithActor, IHudElementWithVisibilityConfig + { + private ExperienceBarConfig Config => (ExperienceBarConfig)_config; + public VisibilityConfig VisibilityConfig => Config.VisibilityConfig; + + public IGameObject? Actor { get; set; } = null; + + private ExperienceHelper _helper = new ExperienceHelper(); + private IconLabelHud _sanctuaryLabel; + + public ExperienceBarHud(ExperienceBarConfig config, string displayName) : base(config, displayName) + { + Config.SanctuaryLabel.IconId = FontAwesomeIcon.Moon; + _sanctuaryLabel = new IconLabelHud(Config.SanctuaryLabel); + } + + protected override (List, List) ChildrenPositionsAndSizes() + { + return (new List() { Config.Position }, new List() { Config.Size }); + } + + public override void DrawChildren(Vector2 origin) + { + if (!Config.Enabled || + Actor is null || + Config.HideWhenInactive && (Plugin.ObjectTable.LocalPlayer?.Level ?? 0) >= 100 || + (Config.HideWhenInactive && Config.HideWhenDownsynced && _helper.IsMaxLevel())) + { + return; + } + + uint current = ExperienceHelper.Instance.CurrentExp; + uint required = ExperienceHelper.Instance.RequiredExp; + uint rested = Config.ShowRestedExp ? ExperienceHelper.Instance.RestedExp : 0; + + // Exp progress bar + PluginConfigColor expFillColor = Config.UseJobColor ? ColorUtils.ColorForActor(Actor) : Config.FillColor; + Rect expBar = BarUtilities.GetFillRect(Config.Position, Config.Size, Config.FillDirection, expFillColor, current, required); + + // Rested exp bar + var restedPos = Config.FillDirection.IsInverted() ? Config.Position : Config.Position + BarUtilities.GetFillDirectionOffset(expBar.Size, Config.FillDirection); + var restedSize = Config.Size - BarUtilities.GetFillDirectionOffset(expBar.Size, Config.FillDirection); + Rect restedBar = BarUtilities.GetFillRect(restedPos, restedSize, Config.FillDirection, Config.RestedExpColor, rested, required, 0f); + + BarHud bar = new BarHud(Config, Actor); + bar.AddForegrounds(expBar, restedBar); + bar.AddLabels(Config.LeftLabel, Config.RightLabel); + + AddDrawActions(bar.GetDrawActions(origin, Config.StrataLevel)); + + // sanctuary icon + if (IsInSanctuary()) + { + AddDrawAction(Config.SanctuaryLabel.StrataLevel, () => + { + var pos = Utils.GetAnchoredPosition(origin, Config.Size, Config.Anchor); + _sanctuaryLabel.Draw(pos + Config.Position, Config.Size, Actor); + }); + } + } + + private unsafe bool IsInSanctuary() + { + AddonExp* addon = ExperienceHelper.Instance.GetExpAddon(); + if (addon == null) { return false; } + AtkImageNode* sanctuaryNode = addon->GetImageNodeById(3); + if (sanctuaryNode == null) { return false; } + + return sanctuaryNode->IsVisible(); + } + } +} diff --git a/Interface/GeneralElements/FontsConfig.cs b/Interface/GeneralElements/FontsConfig.cs new file mode 100644 index 0000000..1a9b41e --- /dev/null +++ b/Interface/GeneralElements/FontsConfig.cs @@ -0,0 +1,401 @@ +using Dalamud.Interface; +using Dalamud.Interface.ImGuiFileDialog; +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Helpers; +using Dalamud.Bindings.ImGui; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Numerics; +using Dalamud.Interface.GameFonts; +using Dalamud.Logging; +using HSUI.Enums; +using HSUI.Interface.Bars; + +namespace HSUI.Interface.GeneralElements +{ + public struct FontData + { + public string Name; + public int Size; + + public FontData(string name, int size) + { + Name = name; + Size = size; + } + } + + [Disableable(false)] + [Section("Customization")] + [SubSection("Fonts", 0)] + public class FontsConfig : PluginConfigObject + { + public new static FontsConfig DefaultConfig() { return new FontsConfig(); } + + public string FontsPath = "C:\\"; + [JsonIgnore] public string ValidatedFontsPath => ValidatePath(FontsPath); + + public SortedList Fonts = new SortedList(); + public bool SupportChineseCharacters = false; + public bool SupportKoreanCharacters = false; + public bool SupportCyrillicCharacters = false; + [JsonIgnore] public readonly Dictionary GameFontMap = new Dictionary() + { + {"Axis", "axis-ffxiv"}, + {"Jupiter", "jupiter-ffxiv"}, + {"JupiterNumeric", "jupiter-numeric-ffxiv"}, + {"MiedingerMid", "meidinger-ffxiv"}, + {"Meidinger", "meidinger-numberic-ffxiv"}, + {"TrumpGothic", "trumpgothic-ffxiv"}, + + }; + + [JsonIgnore] public static readonly List DefaultFontsKeys = new List() { "Expressway_18", "Expressway_14", "Expressway_12" }; + + [JsonIgnore] public static string DefaultBigFontKey => DefaultFontsKeys[0]; + [JsonIgnore] public static string DefaultMediumFontKey => DefaultFontsKeys[1]; + [JsonIgnore] public static string DefaultSmallFontKey => DefaultFontsKeys[2]; + + [JsonIgnore] private int _inputFont = 0; + [JsonIgnore] private int _inputSize = 23; + + [JsonIgnore] private string[] _fonts = null!; + [JsonIgnore] private string[] _sizes = null!; + + [JsonIgnore] private int _removingIndex = -1; + [JsonIgnore] private int _applyingIndex = -1!; + + [JsonIgnore] private FileDialogManager _fileDialogManager = new FileDialogManager(); + + public FontsConfig() + { + ReloadFonts(); + + // default fonts + foreach (string key in DefaultFontsKeys) + { + if (!Fonts.ContainsKey(key)) + { + string[] str = key.Split("_", StringSplitOptions.RemoveEmptyEntries); + var defaultFont = new FontData(str[0], int.Parse(str[1])); + Fonts.Add(key, defaultFont); + } + } + + // sizes + _sizes = new string[100]; + for (int i = 0; i < _sizes.Length; i++) + { + _sizes[i] = (i + 1).ToString(); + } + } + + private bool IsDefaultFont(string key) + { + return DefaultFontsKeys.Contains(key); + } + + private string ValidatePath(string path) + { + if (path.EndsWith("\\") || path.EndsWith("/")) + { + return path; + } + + return path + "\\"; + } + + private string[] FontsFromPath(string path) + { + string[] fonts; + try + { + fonts = Directory.GetFiles(path, "*.ttf"); + } + catch + { + fonts = new string[0]; + } + + for (int i = 0; i < fonts.Length; i++) + { + fonts[i] = fonts[i] + .Replace(path, "") + .Replace(".ttf", "") + .Replace(".TTF", ""); + } + + return fonts; + } + + private string[] FontsFromGame() + { + string[] gameFontArray = Enum.GetNames(typeof(GameFontFamily)).Skip(1).ToArray(); + string[] fonts = new string[gameFontArray.Length]; + + for (int i = 0; i < gameFontArray.Length; i++) + { + fonts[i] = GameFontMap[gameFontArray[i]]; + } + + return fonts; + } + + private void ReloadFonts() + { + string defaultFontsPath = ValidatePath(FontsManager.Instance.DefaultFontsPath); + string[] defaultFonts = FontsFromPath(defaultFontsPath); + string[] gameFonts = FontsFromGame(); + string[] userFonts = FontsFromPath(ValidatedFontsPath); + + _fonts = new string[defaultFonts.Length + gameFonts.Length + userFonts.Length]; + defaultFonts.CopyTo(_fonts, 0); + gameFonts.CopyTo(_fonts, defaultFonts.Length); + userFonts.CopyTo(_fonts, defaultFonts.Length + gameFonts.Length); + } + + private bool AddNewEntry(int font, int size) + { + if (font < 0 || font > _fonts.Length) + { + return false; + } + + if (size <= 0 || size > _sizes.Length) + { + return false; + } + + string fontName = _fonts[font]; + string key = fontName + "_" + size.ToString(); + + if (Fonts.ContainsKey(key)) + { + return false; + } + + FontData fontData = new FontData(fontName, size); + Fonts.Add(key, fontData); + + FontsManager.Instance.BuildFonts(); + + return true; + } + + private void SelectFolder() + { + Action callback = (finished, path) => + { + if (finished && path.Length > 0) + { + FontsPath = path; + ReloadFonts(); + } + }; + + _fileDialogManager.OpenFolderDialog("Select Fonts Folder", callback); + } + + [ManualDraw] + public bool Draw(ref bool changed) + { + ImGuiTableFlags flags = + ImGuiTableFlags.RowBg | + ImGuiTableFlags.Borders | + ImGuiTableFlags.BordersOuter | + ImGuiTableFlags.BordersInner | + ImGuiTableFlags.ScrollY | + ImGuiTableFlags.SizingFixedSame; + + if (ImGui.BeginChild("Fonts", new Vector2(800, 500), false, ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse)) + { + if (_fonts.Length == 0) + { + ImGuiHelper.Tab(); + ImGui.Text("Default font not found in \"%appdata%/Roaming/XIVLauncher/InstalledPlugins/HSUI/Media/Fonts/Expressway.ttf\""); + return false; + } + + ImGuiHelper.NewLineAndTab(); + if (ImGui.InputText("Path", ref FontsPath, 200, ImGuiInputTextFlags.EnterReturnsTrue)) + { + changed = true; + ReloadFonts(); + } + + ImGui.SameLine(); + ImGui.PushFont(UiBuilder.IconFont); + if (ImGui.Button(FontAwesomeIcon.Folder.ToIconString(), new Vector2(0, 0))) + { + SelectFolder(); + } + ImGui.PopFont(); + + ImGuiHelper.Tab(); + ImGui.Combo("Font ##font", ref _inputFont, _fonts, 10); + + ImGui.SameLine(); + ImGui.PushFont(UiBuilder.IconFont); + if (ImGui.Button("\uf2f9", new Vector2(0, 0))) + { + ReloadFonts(); + } + ImGui.PopFont(); + + ImGuiHelper.Tab(); + ImGui.Combo("Size ##size", ref _inputSize, _sizes, 10); + + ImGui.SameLine(); + ImGui.PushFont(UiBuilder.IconFont); + if (ImGui.Button(FontAwesomeIcon.Plus.ToIconString(), new Vector2(0, 0))) + { + changed |= AddNewEntry(_inputFont, _inputSize + 1); + } + ImGui.PopFont(); + + ImGuiHelper.NewLineAndTab(); + if (ImGui.BeginTable("table", 3, flags, new Vector2(326, 300))) + { + ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch, 0, 0); + ImGui.TableSetupColumn("Size", ImGuiTableColumnFlags.WidthFixed, 0, 1); + ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.WidthFixed, 0, 2); + + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableHeadersRow(); + + for (int i = 0; i < Fonts.Count; i++) + { + var key = Fonts.Keys[i]; + var fontData = Fonts.Values[i]; + + ImGui.PushID(i.ToString()); + ImGui.TableNextRow(ImGuiTableRowFlags.None); + + // icon + if (ImGui.TableSetColumnIndex(0)) + { + ImGui.Text(fontData.Name); + } + + // id + if (ImGui.TableSetColumnIndex(1)) + { + ImGui.Text(fontData.Size.ToString()); + } + + // remove + if (ImGui.TableSetColumnIndex(2)) + { + 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.ArrowAltCircleUp.ToIconString())) + { + _applyingIndex = i; + } + + if (!IsDefaultFont(key)) + { + ImGui.SameLine(); + if (ImGui.Button(FontAwesomeIcon.Trash.ToIconString())) + { + _removingIndex = i; + } + } + + ImGui.PopFont(); + ImGui.PopStyleColor(3); + } + ImGui.PopID(); + } + + ImGui.EndTable(); + } + + ImGuiHelper.NewLineAndTab(); + if (ImGui.Checkbox("Support Chinese", ref SupportChineseCharacters)) + { + changed = true; + FontsManager.Instance.BuildFonts(); + } + + ImGui.SameLine(); + if (ImGui.Checkbox("Support Korean", ref SupportKoreanCharacters)) + { + changed = true; + FontsManager.Instance.BuildFonts(); + } + + ImGui.SameLine(); + if (ImGui.Checkbox("Support Cyrillic", ref SupportCyrillicCharacters)) + { + changed = true; + FontsManager.Instance.BuildFonts(); + } + } + + // apply confirmation + if (_applyingIndex >= 0) + { + string[] lines = new string[] { "Are you sure you want to apply this font", "to all labels using a font with the same size?" }; + var (didConfirm, didClose) = ImGuiHelper.DrawConfirmationModal("Apply to all labels?", lines); + + if (didConfirm) + { + var (key, font) = Fonts.ElementAt(_applyingIndex); + + List labelConfigs = ConfigurationManager.Instance.GetObjects(); + foreach (LabelConfig label in labelConfigs) + { + if (label.FontID != null && Fonts.TryGetValue(label.FontID, out FontData value)) + { + if (font.Size == value.Size) + { + label.FontID = key; + } + } + } + + changed = true; + ConfigurationManager.Instance.SaveConfigurations(forced: true); + } + + if (didConfirm || didClose) + { + _applyingIndex = -1; + } + } + + // delete confirmation + if (_removingIndex >= 0) + { + string[] lines = new string[] { "Are you sure you want to remove this font?" }; + var (didConfirm, didClose) = ImGuiHelper.DrawConfirmationModal("Remove custom font?", lines); + + if (didConfirm) + { + Fonts.RemoveAt(_removingIndex); + FontsManager.Instance.BuildFonts(); + changed = true; + } + + if (didConfirm || didClose) + { + _removingIndex = -1; + } + } + + ImGui.EndChild(); + + _fileDialogManager.Draw(); + + return false; + } + } +} diff --git a/Interface/GeneralElements/GCDIndicatorConfig.cs b/Interface/GeneralElements/GCDIndicatorConfig.cs new file mode 100644 index 0000000..036239d --- /dev/null +++ b/Interface/GeneralElements/GCDIndicatorConfig.cs @@ -0,0 +1,96 @@ +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Enums; +using HSUI.Interface.Bars; +using System.Numerics; + +namespace HSUI.Interface.GeneralElements +{ + [DisableParentSettings("Size")] + [Section("Other Elements")] + [SubSection("GCD Indicator", 0)] + public class GCDIndicatorConfig : AnchorablePluginConfigObject + { + [Checkbox("Always Show")] + [Order(3)] + public bool AlwaysShow = false; + + [Checkbox("Anchor To Mouse")] + [Order(4)] + public bool AnchorToMouse = false; + + [ColorEdit4("Background Color")] + [Order(16)] + public PluginConfigColor BackgroundColor = new PluginConfigColor(new Vector4(0f / 255f, 0f / 255f, 0f / 255f, 50f / 100f)); + + [ColorEdit4("Color")] + [Order(17)] + public PluginConfigColor FillColor = new PluginConfigColor(new(220f / 255f, 220f / 255f, 220f / 255f, 100f / 100f)); + + [Checkbox("Show Border")] + [Order(18)] + public bool ShowBorder = true; + + [Checkbox("Instant GCDs only", spacing = true)] + [Order(19)] + public bool InstantGCDsOnly = false; + + [Checkbox("Only show when under GCD Threshold", spacing = true)] + [Order(20)] + public bool LimitGCDThreshold = false; + + [DragFloat("GCD Threshold", velocity = 0.01f)] + [Order(21, collapseWith = nameof(LimitGCDThreshold))] + public float GCDThreshold = 1.50f; + + [Checkbox("Show GCD Queue Indicator", spacing = true)] + [Order(24)] + public bool ShowGCDQueueIndicator = true; + + [ColorEdit4("GCD Queue Color")] + [Order(25, collapseWith = nameof(ShowGCDQueueIndicator))] + public PluginConfigColor QueueColor = new PluginConfigColor(new(13f / 255f, 207f / 255f, 31f / 255f, 100f / 100f)); + + [Checkbox("Circular Mode", spacing = true)] + [Order(30)] + public bool CircularMode = false; + + [DragInt("Radius")] + [Order(35, collapseWith = nameof(CircularMode))] + public int CircleRadius = 40; + + [DragInt("Thickness")] + [Order(40, collapseWith = nameof(CircularMode))] + public int CircleThickness = 10; + + [DragInt("Start Angle", min = 0, max = 359)] + [Order(45, collapseWith = nameof(CircularMode))] + public int CircleStartAngle = 0; + + [Checkbox("Rotate CCW")] + [Order(50, collapseWith = nameof(CircularMode))] + public bool RotateCCW = false; + + [NestedConfig("Bar Mode", 45, collapsingHeader = false)] + public GCDBarConfig Bar = new GCDBarConfig( + new Vector2(0, HUDConstants.BaseHUDOffsetY + 21), + new Vector2(254, 8), + PluginConfigColor.Empty + ); + + [NestedConfig("Visibility", 70)] + public VisibilityConfig VisibilityConfig = new VisibilityConfig(); + + public new static GCDIndicatorConfig DefaultConfig() { return new GCDIndicatorConfig() { Enabled = false, Strata = StrataLevel.MID_HIGH }; } + } + + [DisableParentSettings("Position", "Anchor", "HideWhenInactive", "FillColor", "BackgroundColor", "DrawBorder")] + [Exportable(false)] + public class GCDBarConfig : BarConfig + { + public GCDBarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor, BarDirection fillDirection = BarDirection.Right) + : base(position, size, fillColor, fillDirection) + { + } + } +} diff --git a/Interface/GeneralElements/GCDIndicatorHud.cs b/Interface/GeneralElements/GCDIndicatorHud.cs new file mode 100644 index 0000000..dcc12e9 --- /dev/null +++ b/Interface/GeneralElements/GCDIndicatorHud.cs @@ -0,0 +1,247 @@ +using System; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using Dalamud.Bindings.ImGui; +using System.Collections.Generic; +using System.Numerics; +using HSUI.Config; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Logging; + +namespace HSUI.Interface.GeneralElements +{ + public class GCDIndicatorHud : DraggableHudElement, IHudElementWithActor, IHudElementWithVisibilityConfig + { + private GCDIndicatorConfig Config => (GCDIndicatorConfig)_config; + public VisibilityConfig VisibilityConfig => Config.VisibilityConfig; + + public IGameObject? Actor { get; set; } = null; + + private bool _wasBarEnabled = true; + private bool _wasCircularModeEnabled = false; + private float _lastTotalCastTime = 0; + + public GCDIndicatorHud(GCDIndicatorConfig config, string displayName) : base(config, displayName) { } + + protected override (List, List) ChildrenPositionsAndSizes() + { + var (pos, size) = GetPositionAndSize(Vector2.Zero); + + if (Config.CircularMode) + { + pos -= size / 2f; + } + + return (new List() { pos }, new List() { size }); + } + + private (Vector2, Vector2) GetPositionAndSize(Vector2 origin) + { + Vector2 pos = Config.AnchorToMouse ? ImGui.GetMousePos() + Config.Position : origin + Config.Position; + Vector2 size = Config.Bar.Size; + + if (Config.CircularMode) + { + size = new Vector2(Config.CircleRadius * 2, Config.CircleRadius * 2); + pos += size / 2f; + } + + return (pos, size); + } + + protected override void DrawDraggableArea(Vector2 origin) + { + if (Config.AnchorToMouse) + { + return; + } + + base.DrawDraggableArea(origin); + } + + public override void DrawChildren(Vector2 origin) + { + CheckToggles(); + + if (!Config.Enabled || Actor == null || Actor is not IPlayerCharacter) + { + return; + } + + GCDHelper.GetGCDInfo((IPlayerCharacter)Actor, out var elapsed, out var total); + + if (!Config.AlwaysShow && total == 0) + { + _lastTotalCastTime = 0; + return; + } + + if (_lastTotalCastTime == 0 && Utils.IsActorCasting(Actor)) + { + _lastTotalCastTime = ((IBattleChara)Actor).TotalCastTime; + } + + var scale = elapsed / total; + if (scale <= 0) + { + _lastTotalCastTime = 0; + return; + } + + bool instantGCDsOnly = Config.InstantGCDsOnly && _lastTotalCastTime != 0; + bool thresholdGCDs = Config.LimitGCDThreshold && _lastTotalCastTime > Config.GCDThreshold; + + if (instantGCDsOnly || thresholdGCDs) + { + if (Config.AlwaysShow) + { + elapsed = 0; + total = 0; + } + else + { + return; + } + } + + Config.Bar.Position = Config.Position; + Config.Bar.Anchor = Config.Anchor; + Config.Bar.BackgroundColor = Config.BackgroundColor; + Config.Bar.FillColor = Config.FillColor; + Config.Bar.DrawBorder = Config.ShowBorder; + + if (Config.Bar.Enabled) + { + DrawNormalBar(origin, elapsed, total); + } + else + { + var (pos, size) = GetPositionAndSize(origin); + pos = Utils.GetAnchoredPosition(pos, size, Config.Anchor); + + AddDrawAction(_config.StrataLevel, () => + { + DrawCircularIndicator(pos, Config.CircleRadius, elapsed, total); + }); + } + } + + private void CheckToggles() + { + bool barEnabledChanged = _wasBarEnabled != Config.Bar.Enabled; + if (barEnabledChanged) + { + Config.CircularMode = !Config.Bar.Enabled; + } + else + { + bool circularModeChanged = _wasCircularModeEnabled != Config.CircularMode; + if (circularModeChanged) + { + Config.Bar.Enabled = !Config.CircularMode; + } + } + + _wasBarEnabled = Config.Bar.Enabled; + _wasCircularModeEnabled = Config.CircularMode; + } + + private void DrawCircularIndicator(Vector2 position, float radius, float current, float total) + { + total = Config.AlwaysShow && total == 0 ? 1 : total; + current = Config.AlwaysShow && current == 0 ? total : current; + + var size = new Vector2(radius * 2); + DrawHelper.DrawInWindow(ID, position - size / 2, size, false, (drawList) => + { + current = Math.Min(current, total); + + // controls how smooth the arc looks + const int segments = 100; + const float queueTime = 0.5f; + float startAngle = 0f; + float endAngle = 2f * (float)Math.PI; + float offset = (float)(-Math.PI / 2f + (Config.CircleStartAngle * (Math.PI / 180f))); + + if (Config.RotateCCW) + { + startAngle *= -1; + endAngle *= -1; + } + + if (Config.AlwaysShow && current == total) + { + drawList.PathArcTo(position, radius, startAngle + offset, endAngle + offset, segments); + drawList.PathStroke(Config.FillColor.Base, ImDrawFlags.None, Config.CircleThickness); + } + else + { + // always draw until the queue threshold + float progressAngle = Math.Min(current, total - (Config.ShowGCDQueueIndicator ? queueTime : 0f)) / total * endAngle; + + // drawing an arc with thickness to make it look like an annular sector + drawList.PathArcTo(position, radius, startAngle + offset, progressAngle + offset, segments); + drawList.PathStroke(Config.FillColor.Base, ImDrawFlags.None, Config.CircleThickness); + + // draw the queue indicator + if (Config.ShowGCDQueueIndicator && current > total - queueTime) + { + float oldAngle = progressAngle - 0.0003f * total * endAngle; + progressAngle = current / total * endAngle; + drawList.PathArcTo(position, radius, oldAngle + offset, progressAngle + offset, segments); + drawList.PathStroke(Config.QueueColor.Base, ImDrawFlags.None, Config.CircleThickness); + } + + // anything that remains is background + drawList.PathArcTo(position, radius, progressAngle + offset, endAngle + offset, segments); + drawList.PathStroke(Config.BackgroundColor.Base, ImDrawFlags.None, Config.CircleThickness); + } + + if (Config.ShowBorder) + { + drawList.PathArcTo(position, radius - Config.CircleThickness / 2f, 0, endAngle, segments); + drawList.PathStroke(0xFF000000, ImDrawFlags.None, 1); + + drawList.PathArcTo(position, radius + Config.CircleThickness / 2f, 0, endAngle, segments); + drawList.PathStroke(0xFF000000, ImDrawFlags.None, 1); + } + }); + } + + private void DrawNormalBar(Vector2 origin, float current, float total) + { + GCDBarConfig config = Config.Bar; + + Rect mainRect = BarUtilities.GetFillRect(config.Position, config.Size, config.FillDirection, config.FillColor, current, total, 0); + BarHud bar = new BarHud(config, null, null); + bar.AddForegrounds(mainRect); + + float currentPercent = current / total; + float percentNonQueue = total != 0 ? 1F - (500f / 1000f) / total : 0; + + if (percentNonQueue > 0 && currentPercent >= percentNonQueue && Config.ShowGCDQueueIndicator) + { + float scale = 1 - percentNonQueue; + Vector2 size = config.FillDirection.IsHorizontal() ? + new Vector2(config.Size.X * scale, config.Size.Y) : + new Vector2(config.Size.X, config.Size.Y * scale); + + Vector2 pos = config.Position; + if (config.FillDirection == BarDirection.Right) + { + pos.X += config.Size.X * percentNonQueue; + } + else if (config.FillDirection == BarDirection.Down) + { + pos.Y += config.Size.Y * percentNonQueue; + } + + Rect foreground = BarUtilities.GetFillRect(pos, size, config.FillDirection, Config.QueueColor, currentPercent - percentNonQueue, scale, 0); + bar.AddForegrounds(foreground); + } + + AddDrawActions(bar.GetDrawActions(origin, _config.StrataLevel)); + } + } +} diff --git a/Interface/GeneralElements/GlobalColors.cs b/Interface/GeneralElements/GlobalColors.cs new file mode 100644 index 0000000..6f4d904 --- /dev/null +++ b/Interface/GeneralElements/GlobalColors.cs @@ -0,0 +1,478 @@ +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Enums; +using HSUI.Helpers; +using HSUI.Interface.Party; +using System; +using System.Collections.Generic; + +namespace HSUI.Interface.GeneralElements +{ + public class GlobalColors : IDisposable + { + #region Singleton + private MiscColorConfig _miscColorConfig = null!; + private RolesColorConfig _rolesColorConfig = null!; + + private Dictionary ColorMap = null!; + + private GlobalColors() + { + ConfigurationManager.Instance.ResetEvent += OnConfigReset; + OnConfigReset(ConfigurationManager.Instance); + } + + private void OnConfigReset(ConfigurationManager sender) + { + _miscColorConfig = sender.GetConfigObject(); + _rolesColorConfig = sender.GetConfigObject(); + + var tanksColorConfig = sender.GetConfigObject(); + var healersColorConfig = sender.GetConfigObject(); + var meleeColorConfig = sender.GetConfigObject(); + var rangedColorConfig = sender.GetConfigObject(); + var castersColorConfig = sender.GetConfigObject(); + + ColorMap = new Dictionary() + { + // tanks + [JobIDs.GLA] = tanksColorConfig.GLAColor, + [JobIDs.MRD] = tanksColorConfig.MRDColor, + [JobIDs.PLD] = tanksColorConfig.PLDColor, + [JobIDs.WAR] = tanksColorConfig.WARColor, + [JobIDs.DRK] = tanksColorConfig.DRKColor, + [JobIDs.GNB] = tanksColorConfig.GNBColor, + + // healers + [JobIDs.CNJ] = healersColorConfig.CNJColor, + [JobIDs.WHM] = healersColorConfig.WHMColor, + [JobIDs.SCH] = healersColorConfig.SCHColor, + [JobIDs.AST] = healersColorConfig.ASTColor, + [JobIDs.SGE] = healersColorConfig.SGEColor, + + // melee + [JobIDs.PGL] = meleeColorConfig.PGLColor, + [JobIDs.LNC] = meleeColorConfig.LNCColor, + [JobIDs.ROG] = meleeColorConfig.ROGColor, + [JobIDs.MNK] = meleeColorConfig.MNKColor, + [JobIDs.DRG] = meleeColorConfig.DRGColor, + [JobIDs.NIN] = meleeColorConfig.NINColor, + [JobIDs.SAM] = meleeColorConfig.SAMColor, + [JobIDs.RPR] = meleeColorConfig.RPRColor, + [JobIDs.VPR] = meleeColorConfig.VPRColor, + + // ranged + [JobIDs.ARC] = rangedColorConfig.ARCColor, + [JobIDs.BRD] = rangedColorConfig.BRDColor, + [JobIDs.MCH] = rangedColorConfig.MCHColor, + [JobIDs.DNC] = rangedColorConfig.DNCColor, + + // casters + [JobIDs.THM] = castersColorConfig.THMColor, + [JobIDs.ACN] = castersColorConfig.ACNColor, + [JobIDs.BLM] = castersColorConfig.BLMColor, + [JobIDs.SMN] = castersColorConfig.SMNColor, + [JobIDs.RDM] = castersColorConfig.RDMColor, + [JobIDs.PCT] = castersColorConfig.PCTColor, + [JobIDs.BLU] = castersColorConfig.BLUColor, + + // crafters + [JobIDs.CRP] = _rolesColorConfig.HANDColor, + [JobIDs.BSM] = _rolesColorConfig.HANDColor, + [JobIDs.ARM] = _rolesColorConfig.HANDColor, + [JobIDs.GSM] = _rolesColorConfig.HANDColor, + [JobIDs.LTW] = _rolesColorConfig.HANDColor, + [JobIDs.WVR] = _rolesColorConfig.HANDColor, + [JobIDs.ALC] = _rolesColorConfig.HANDColor, + [JobIDs.CUL] = _rolesColorConfig.HANDColor, + + // gatherers + [JobIDs.MIN] = _rolesColorConfig.LANDColor, + [JobIDs.BOT] = _rolesColorConfig.LANDColor, + [JobIDs.FSH] = _rolesColorConfig.LANDColor + }; + } + + public static void Initialize() + { + Instance = new GlobalColors(); + } + + public static GlobalColors Instance { get; private set; } = null!; + + ~GlobalColors() + { + 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 PluginConfigColor? ColorForJobId(uint jobId) => ColorMap.TryGetValue(jobId, out PluginConfigColor? color) ? color : null; + + public PluginConfigColor SafeColorForJobId(uint jobId) => ColorForJobId(jobId) ?? _miscColorConfig.NPCNeutralColor; + + public PluginConfigColor? RoleColorForJobId(uint jobId) + { + JobRoles role = JobsHelper.RoleForJob(jobId); + + return role switch + { + JobRoles.Tank => _rolesColorConfig.TankRoleColor, + JobRoles.Healer => _rolesColorConfig.HealerRoleColor, + JobRoles.DPSMelee => _rolesColorConfig.UseSpecificDPSColors ? _rolesColorConfig.MeleeDPSRoleColor : _rolesColorConfig.DPSRoleColor, + JobRoles.DPSRanged => _rolesColorConfig.UseSpecificDPSColors ? _rolesColorConfig.RangedDPSRoleColor : _rolesColorConfig.DPSRoleColor, + JobRoles.DPSCaster => _rolesColorConfig.UseSpecificDPSColors ? _rolesColorConfig.CasterDPSRoleColor : _rolesColorConfig.DPSRoleColor, + JobRoles.Gatherer => _rolesColorConfig.LANDColor, + JobRoles.Crafter => _rolesColorConfig.HANDColor, + _ => null + }; + } + + public PluginConfigColor SafeRoleColorForJobId(uint jobId) => RoleColorForJobId(jobId) ?? _miscColorConfig.NPCNeutralColor; + + public PluginConfigColor EmptyUnitFrameColor => _miscColorConfig.EmptyUnitFrameColor; + public PluginConfigColor EmptyColor => _miscColorConfig.EmptyColor; + public PluginConfigColor PartialFillColor => _miscColorConfig.PartialFillColor; + public PluginConfigColor NPCFriendlyColor => _miscColorConfig.NPCFriendlyColor; + public PluginConfigColor NPCHostileColor => _miscColorConfig.NPCHostileColor; + public PluginConfigColor NPCNeutralColor => _miscColorConfig.NPCNeutralColor; + } + + [Disableable(false)] + [Section("Colors")] + [SubSection("Tanks", 0)] + public class TanksColorConfig : PluginConfigObject + { + public new static TanksColorConfig DefaultConfig() { return new TanksColorConfig(); } + + [ColorEdit4("Paladin", spacing = true)] + [Order(5)] + public PluginConfigColor PLDColor = new PluginConfigColor(new(168f / 255f, 210f / 255f, 230f / 255f, 100f / 100f)); + + [ColorEdit4("Dark Knight")] + [Order(10)] + public PluginConfigColor DRKColor = new PluginConfigColor(new(209f / 255f, 38f / 255f, 204f / 255f, 100f / 100f)); + + [ColorEdit4("Warrior")] + [Order(15)] + public PluginConfigColor WARColor = new PluginConfigColor(new(207f / 255f, 38f / 255f, 33f / 255f, 100f / 100f)); + + [ColorEdit4("Gunbreaker")] + [Order(20)] + public PluginConfigColor GNBColor = new PluginConfigColor(new(121f / 255f, 109f / 255f, 48f / 255f, 100f / 100f)); + + [ColorEdit4("Gladiator", spacing = true)] + [Order(25)] + public PluginConfigColor GLAColor = new PluginConfigColor(new(168f / 255f, 210f / 255f, 230f / 255f, 100f / 100f)); + + [ColorEdit4("Marauder")] + [Order(30)] + public PluginConfigColor MRDColor = new PluginConfigColor(new(207f / 255f, 38f / 255f, 33f / 255f, 100f / 100f)); + } + + [Disableable(false)] + [Section("Colors")] + [SubSection("Healers", 0)] + public class HealersColorConfig : PluginConfigObject + { + public new static HealersColorConfig DefaultConfig() { return new HealersColorConfig(); } + + [ColorEdit4("Scholar", spacing = true)] + [Order(5)] + public PluginConfigColor SCHColor = new PluginConfigColor(new(134f / 255f, 87f / 255f, 255f / 255f, 100f / 100f)); + + [ColorEdit4("White Mage")] + [Order(10)] + public PluginConfigColor WHMColor = new PluginConfigColor(new(255f / 255f, 240f / 255f, 220f / 255f, 100f / 100f)); + + [ColorEdit4("Astrologian")] + [Order(15)] + public PluginConfigColor ASTColor = new PluginConfigColor(new(255f / 255f, 231f / 255f, 74f / 255f, 100f / 100f)); + + [ColorEdit4("Sage")] + [Order(20)] + public PluginConfigColor SGEColor = new PluginConfigColor(new(144f / 255f, 176f / 255f, 255f / 255f, 100f / 100f)); + + [ColorEdit4("Conjurer", spacing = true)] + [Order(25)] + public PluginConfigColor CNJColor = new PluginConfigColor(new(255f / 255f, 240f / 255f, 220f / 255f, 100f / 100f)); + } + + [Disableable(false)] + [Section("Colors")] + [SubSection("Melee", 0)] + public class MeleeColorConfig : PluginConfigObject + { + public new static MeleeColorConfig DefaultConfig() { return new MeleeColorConfig(); } + + [ColorEdit4("Monk", spacing = true)] + [Order(5)] + public PluginConfigColor MNKColor = new PluginConfigColor(new(214f / 255f, 156f / 255f, 0f / 255f, 100f / 100f)); + + [ColorEdit4("Ninja")] + [Order(10)] + public PluginConfigColor NINColor = new PluginConfigColor(new(175f / 255f, 25f / 255f, 100f / 255f, 100f / 100f)); + + [ColorEdit4("Dragoon")] + [Order(15)] + public PluginConfigColor DRGColor = new PluginConfigColor(new(65f / 255f, 100f / 255f, 205f / 255f, 100f / 100f)); + + [ColorEdit4("Samurai")] + [Order(20)] + public PluginConfigColor SAMColor = new PluginConfigColor(new(228f / 255f, 109f / 255f, 4f / 255f, 100f / 100f)); + + [ColorEdit4("Reaper")] + [Order(25)] + public PluginConfigColor RPRColor = new PluginConfigColor(new(150f / 255f, 90f / 255f, 144f / 255f, 100f / 100f)); + + [ColorEdit4("Viper")] + [Order(25)] + public PluginConfigColor VPRColor = new PluginConfigColor(new(16f / 255f, 130f / 255f, 16f / 255f, 100f / 100f)); + + [ColorEdit4("Pugilist", spacing = true)] + [Order(30)] + public PluginConfigColor PGLColor = new PluginConfigColor(new(214f / 255f, 156f / 255f, 0f / 255f, 100f / 100f)); + + [ColorEdit4("Rogue")] + [Order(35)] + public PluginConfigColor ROGColor = new PluginConfigColor(new(175f / 255f, 25f / 255f, 100f / 255f, 100f / 100f)); + + [ColorEdit4("Lancer")] + [Order(40)] + public PluginConfigColor LNCColor = new PluginConfigColor(new(65f / 255f, 100f / 255f, 205f / 255f, 100f / 100f)); + } + + [Disableable(false)] + [Section("Colors")] + [SubSection("Ranged", 0)] + public class RangedColorConfig : PluginConfigObject + { + public new static RangedColorConfig DefaultConfig() { return new RangedColorConfig(); } + + [ColorEdit4("Bard", spacing = true)] + [Order(5)] + public PluginConfigColor BRDColor = new PluginConfigColor(new(145f / 255f, 186f / 255f, 94f / 255f, 100f / 100f)); + + [ColorEdit4("Machinist")] + [Order(10)] + public PluginConfigColor MCHColor = new PluginConfigColor(new(110f / 255f, 225f / 255f, 214f / 255f, 100f / 100f)); + + [ColorEdit4("Dancer")] + [Order(15)] + public PluginConfigColor DNCColor = new PluginConfigColor(new(226f / 255f, 176f / 255f, 175f / 255f, 100f / 100f)); + + [ColorEdit4("Archer", separator = true)] + [Order(20)] + public PluginConfigColor ARCColor = new PluginConfigColor(new(145f / 255f, 186f / 255f, 94f / 255f, 100f / 100f)); + } + + [Disableable(false)] + [Section("Colors")] + [SubSection("Caster", 0)] + public class CastersColorConfig : PluginConfigObject + { + public new static CastersColorConfig DefaultConfig() { return new CastersColorConfig(); } + + [ColorEdit4("Black Mage", spacing = true)] + [Order(5)] + public PluginConfigColor BLMColor = new PluginConfigColor(new(165f / 255f, 121f / 255f, 214f / 255f, 100f / 100f)); + + [ColorEdit4("Summoner")] + [Order(10)] + public PluginConfigColor SMNColor = new PluginConfigColor(new(45f / 255f, 155f / 255f, 120f / 255f, 100f / 100f)); + + [ColorEdit4("Red Mage")] + [Order(15)] + public PluginConfigColor RDMColor = new PluginConfigColor(new(232f / 255f, 123f / 255f, 123f / 255f, 100f / 100f)); + + [ColorEdit4("Pictomancer")] + [Order(15)] + public PluginConfigColor PCTColor = new PluginConfigColor(new(252f / 255f, 146f / 255f, 225f / 255f, 100f / 100f)); + + [ColorEdit4("Blue Mage", spacing = true)] + [Order(20)] + public PluginConfigColor BLUColor = new PluginConfigColor(new(0f / 255f, 185f / 255f, 247f / 255f, 100f / 100f)); + + [ColorEdit4("Thaumaturge")] + [Order(25)] + public PluginConfigColor THMColor = new PluginConfigColor(new(165f / 255f, 121f / 255f, 214f / 255f, 100f / 100f)); + + [ColorEdit4("Arcanist")] + [Order(30)] + public PluginConfigColor ACNColor = new PluginConfigColor(new(45f / 255f, 155f / 255f, 120f / 255f, 100f / 100f)); + } + + [Disableable(false)] + [Section("Colors")] + [SubSection("Roles", 0)] + public class RolesColorConfig : PluginConfigObject + { + public new static RolesColorConfig DefaultConfig() { return new RolesColorConfig(); } + + [ColorEdit4("Tank")] + [Order(10)] + public PluginConfigColor TankRoleColor = new PluginConfigColor(new(21f / 255f, 28f / 255f, 100f / 255f, 100f / 100f)); + + [ColorEdit4("DPS")] + [Order(15)] + public PluginConfigColor DPSRoleColor = new PluginConfigColor(new(153f / 255f, 23f / 255f, 23f / 255f, 100f / 100f)); + + [ColorEdit4("Healer")] + [Order(20)] + public PluginConfigColor HealerRoleColor = new PluginConfigColor(new(46f / 255f, 125f / 255f, 50f / 255f, 100f / 100f)); + + [ColorEdit4("Disciple of the Land", spacing = true)] + [Order(25)] + public PluginConfigColor LANDColor = new PluginConfigColor(new(99f / 255f, 172f / 255f, 14f / 255f, 100f / 100f)); + + [ColorEdit4("Disciple of the Hand")] + [Order(30)] + public PluginConfigColor HANDColor = new PluginConfigColor(new(99f / 255f, 172f / 255f, 14f / 255f, 100f / 100f)); + + [Checkbox("Use Specific DPS Colors", spacing = true)] + [Order(35)] + public bool UseSpecificDPSColors = false; + + [ColorEdit4("Melee DPS")] + [Order(40, collapseWith = nameof(UseSpecificDPSColors))] + public PluginConfigColor MeleeDPSRoleColor = new PluginConfigColor(new(151f / 255f, 56f / 255f, 56f / 255f, 100f / 100f)); + + [ColorEdit4("Ranged DPS")] + [Order(40, collapseWith = nameof(UseSpecificDPSColors))] + public PluginConfigColor RangedDPSRoleColor = new PluginConfigColor(new(250f / 255f, 185f / 255f, 67f / 255f, 100f / 100f)); + + [ColorEdit4("Caster DPS")] + [Order(40, collapseWith = nameof(UseSpecificDPSColors))] + public PluginConfigColor CasterDPSRoleColor = new PluginConfigColor(new(154f / 255f, 82f / 255f, 193f / 255f, 100f / 100f)); + } + + [Disableable(false)] + [Section("Colors")] + [SubSection("Misc", 0)] + public class MiscColorConfig : PluginConfigObject + { + public new static MiscColorConfig DefaultConfig() { return new MiscColorConfig(); } + + [Combo("Gradient Type For Bars", "Flat Color", "Right", "Left", "Up", "Down", "Centered Horizontal", spacing = true)] + [Order(4)] + public GradientDirection GradientDirection = GradientDirection.Down; + + [ColorEdit4("Empty Unit Frame", separator = true)] + [Order(5)] + public PluginConfigColor EmptyUnitFrameColor = new PluginConfigColor(new(0f / 255f, 0f / 255f, 0f / 255f, 95f / 100f)); + + [ColorEdit4("Empty Bar")] + [Order(10)] + public PluginConfigColor EmptyColor = new PluginConfigColor(new(0f / 255f, 0f / 255f, 0f / 255f, 50f / 100f)); + + [ColorEdit4("Partially Filled Bar")] + [Order(15)] + public PluginConfigColor PartialFillColor = new PluginConfigColor(new(180f / 255f, 180f / 255f, 180f / 255f, 100f / 100f)); + + [ColorEdit4("NPC Friendly", separator = true)] + [Order(20)] + public PluginConfigColor NPCFriendlyColor = new PluginConfigColor(new(99f / 255f, 172f / 255f, 14f / 255f, 100f / 100f)); + + [ColorEdit4("NPC Hostile")] + [Order(25)] + public PluginConfigColor NPCHostileColor = new PluginConfigColor(new(233f / 255f, 4f / 255f, 4f / 255f, 100f / 100f)); + + [ColorEdit4("NPC Neutral")] + [Order(30)] + public PluginConfigColor NPCNeutralColor = new PluginConfigColor(new(218f / 255f, 157f / 255f, 46f / 255f, 100f / 100f)); + } + + [Exportable(false)] + public class ColorByHealthValueConfig : PluginConfigObject + { + + [Checkbox("Use Max Health Color")] + [Order(5)] + public bool UseMaxHealthColor = false; + + [ColorEdit4("Max Health Color")] + [Order(10, collapseWith = nameof(UseMaxHealthColor))] + public PluginConfigColor MaxHealthColor = new PluginConfigColor(new(18f / 255f, 18f / 255f, 18f / 255f, 100f / 100f)); + + [Checkbox("Job Color as Max Health Color")] + [Order(15, collapseWith = nameof(UseMaxHealthColor))] + public bool UseJobColorAsMaxHealth = false; + + [Checkbox("Job Role as Max Health Color")] + [Order(20, collapseWith = nameof(UseMaxHealthColor))] + public bool UseRoleColorAsMaxHealth = false; + + [ColorEdit4("High Health Color")] + [Order(25)] + public PluginConfigColor FullHealthColor = new PluginConfigColor(new(0f / 255f, 255f / 255f, 0f / 255f, 100f / 100f)); + + [ColorEdit4("Low Health Color")] + [Order(30)] + public PluginConfigColor LowHealthColor = new PluginConfigColor(new(255f / 255f, 0f / 255f, 0f / 255f, 100f / 100f)); + + [DragFloat("Max Health Color Above Health %", min = 50f, max = 100f, velocity = 1f)] + [Order(35)] + public float FullHealthColorThreshold = 75f; + + [DragFloat("Low Health Color Below Health %", min = 0f, max = 50f, velocity = 1f)] + [Order(40)] + public float LowHealthColorThreshold = 25f; + + [Combo("Blend Mode", "LAB", "LChab", "XYZ", "RGB", "LChuv", "Luv", "Jzazbz", "JzCzhz")] + [Order(45)] + public BlendMode BlendMode = BlendMode.LAB; + } + + public class ColorByHealthFieldsConverter : PluginConfigObjectConverter + { + public ColorByHealthFieldsConverter() + { + SameTypeFieldConverter enabled = + new SameTypeFieldConverter("ColorByHealth.Enabled", false); + FieldConvertersMap.Add("UseColorBasedOnHealthValue", enabled); + + SameClassFieldConverter fullHealth = + new SameClassFieldConverter("ColorByHealth.FullHealthColor", new PluginConfigColor(new(0f / 255f, 255f / 255f, 0f / 255f, 100f / 100f))); + FieldConvertersMap.Add("FullHealthColor", fullHealth); + + SameClassFieldConverter lowHealth = + new SameClassFieldConverter("ColorByHealth.LowHealthColor", new PluginConfigColor(new(255f / 255f, 0f / 255f, 0f / 255f, 100f / 100f))); + FieldConvertersMap.Add("LowHealthColor", lowHealth); + + SameTypeFieldConverter fullThreshold = new SameTypeFieldConverter("ColorByHealth.FullHealthColorThreshold", 75f); + FieldConvertersMap.Add("FullHealthColorThreshold", fullThreshold); + + SameTypeFieldConverter lowThreshold = new SameTypeFieldConverter("ColorByHealth.LowHealthColorThreshold", 25f); + FieldConvertersMap.Add("LowHealthColorThreshold", lowThreshold); + + SameTypeFieldConverter blendMode = new SameTypeFieldConverter("ColorByHealth.BlendMode", BlendMode.LAB); + FieldConvertersMap.Add("blendMode", blendMode); + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(PartyFramesColorsConfig) || + objectType == typeof(UnitFrameConfig) || + objectType == typeof(PlayerUnitFrameConfig) || + objectType == typeof(TargetUnitFrameConfig) || + objectType == typeof(TargetOfTargetUnitFrameConfig) || + objectType == typeof(FocusTargetUnitFrameConfig); + } + } +} diff --git a/Interface/GeneralElements/GlobalVisibilityConfig.cs b/Interface/GeneralElements/GlobalVisibilityConfig.cs new file mode 100644 index 0000000..821f3f8 --- /dev/null +++ b/Interface/GeneralElements/GlobalVisibilityConfig.cs @@ -0,0 +1,54 @@ +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Helpers; +using Dalamud.Bindings.ImGui; +using Newtonsoft.Json; +using System.Numerics; + +namespace HSUI.Interface.GeneralElements +{ + [Disableable(false)] + [Exportable(false)] + [Section("Visibility")] + [SubSection("Global", 0)] + public class GlobalVisibilityConfig : PluginConfigObject + { + public new static GlobalVisibilityConfig DefaultConfig() { return new GlobalVisibilityConfig(); } + + [NestedConfig("Visibility", 50, collapsingHeader = false)] + public VisibilityConfig VisibilityConfig = new VisibilityConfig(); + + [JsonIgnore] + private bool _applying = false; + + [ManualDraw] + public bool Draw(ref bool changed) + { + ImGui.NewLine(); + + if (ImGui.Button("Apply to all elements", new Vector2(200, 30))) + { + _applying = true; + } + + if (_applying) + { + string[] lines = new string[] { "This will replace the visibility settings", "for ALL HSUI elements!", "Are you sure?" }; + var (didConfirm, didClose) = ImGuiHelper.DrawConfirmationModal("Apply?", lines); + + if (didConfirm) + { + ConfigurationManager.Instance.OnGlobalVisibilityChanged(VisibilityConfig); + changed = true; + } + + if (didConfirm || didClose) + { + _applying = false; + } + } + + return false; + } + } +} diff --git a/Interface/GeneralElements/GridConfig.cs b/Interface/GeneralElements/GridConfig.cs new file mode 100644 index 0000000..42ec9e1 --- /dev/null +++ b/Interface/GeneralElements/GridConfig.cs @@ -0,0 +1,42 @@ +using HSUI.Config; +using HSUI.Config.Attributes; + +namespace HSUI.Interface.GeneralElements +{ + [Exportable(false)] + [Section("Misc")] + [SubSection("Grid", 0)] + public class GridConfig : PluginConfigObject + { + public new static GridConfig DefaultConfig() + { + var config = new GridConfig(); + config.Enabled = false; + + return config; + } + + [DragFloat("Background Alpha", min = 0, max = 1, velocity = .05f)] + [Order(10)] + public float BackgroundAlpha = 0.3f; + + [Checkbox("Show Center Lines")] + [Order(15)] + public bool ShowCenterLines = true; + [Checkbox("Show Anchor Points")] + [Order(20)] + + public bool ShowAnchorPoints = true; + [Checkbox("Grid Divisions", spacing = true)] + [Order(25)] + public bool ShowGrid = true; + + [DragInt("Divisions Distance", min = 50, max = 500)] + [Order(30, collapseWith = nameof(ShowGrid))] + public int GridDivisionsDistance = 50; + + [DragInt("Subdivision Count", min = 1, max = 10)] + [Order(35, collapseWith = nameof(ShowGrid))] + public int GridSubdivisionCount = 4; + } +} diff --git a/Interface/GeneralElements/HUDOptionsConfig.cs b/Interface/GeneralElements/HUDOptionsConfig.cs new file mode 100644 index 0000000..6eb5a18 --- /dev/null +++ b/Interface/GeneralElements/HUDOptionsConfig.cs @@ -0,0 +1,132 @@ +using HSUI.Config; +using HSUI.Config.Attributes; +using System; +using System.Collections.Generic; +using System.Numerics; + +namespace HSUI.Interface.GeneralElements +{ + [Disableable(false)] + [Section("Misc")] + [SubSection("HUD Options", 0)] + public class HUDOptionsConfig : PluginConfigObject + { + [Checkbox("Global HUD Position")] + [Order(5)] + public bool UseGlobalHudShift = false; + + [DragInt2("Position", min = -4000, max = 4000)] + [Order(6, collapseWith = nameof(UseGlobalHudShift))] + public Vector2 HudOffset = new(0, 0); + + [Checkbox("Dim HSUI's settings window when not focused")] + [Order(10)] + public bool DimConfigWindow = false; + + [Checkbox("Automatically disable HUD elements preview", help = "If enabled, all HUD elements preview modes are disabled when HSUI's setting window is closed.")] + [Order(11)] + public bool AutomaticPreviewDisabling = true; + + [Checkbox("Use HSUI style", help = "If enabled, HSUI will use its own style for the setting window instead of the general Dalamud style.")] + [Order(12)] + public bool OverrideDalamudStyle = true; + + [Checkbox("Mouseover", separator = true)] + [Order(15)] + public bool MouseoverEnabled = true; + + [Checkbox("Automatic Mode", help = + "When enabled: All your actions will automatically assume mouseover when your cursor is on top of a unit frame.\n" + + "Mouseover macros or other mouseover plugins are not necessary and WON'T WORK in this mode!\n\n" + + "When disabled: HSUI unit frames will behave like the game's ones.\n" + + "You'll need to use mouseover macros or other mouseover related plugins in this mode.")] + [Order(16, collapseWith = nameof(MouseoverEnabled))] + public bool MouseoverAutomaticMode = true; + + //[Checkbox("Support Special Mouse Clicks", isMonitored = true, spacing = true, help = + // "When enabled HSUI will attempt to support special mouse binds (mousewheel, M4, M5, etc) when the cursor\n" + + // "is hovering on top of HSUI's unit frames.\n\n" + + // "If you don't have actions bound to these mouse buttons, it is adviced that you leave this feature disabled.\n\n" + + // "This feature can cause some issues such as click inputs not working in HSUI, or through out the game.\n" + + // "If you run into these kinds of issues, you can try reloading HSUI, restarting the game, or disabling this feature.")] + //[Order(17)] + public bool InputsProxyEnabled = false; + + [Checkbox("Hide Default HUD When Replaced", isMonitored = true, separator = true, help = + "When enabled, HSUI automatically hides the default game HUD elements that HSUI replaces.\n" + + "For example: when HSUI hotbars are on, game hotbars are hidden; when HSUI unit frames are on, game parameter/target bars are hidden; etc.")] + [Order(38)] + public bool HideDefaultHudWhenReplaced = true; + + [Checkbox("Hide Default Job Gauges", isMonitored = true)] + [Order(40)] + public bool HideDefaultJobGauges = false; + + [Checkbox("Hide Default Castbar", isMonitored = true)] + [Order(45)] + public bool HideDefaultCastbar = false; + + [Checkbox("Hide Default Pulltimer", isMonitored = true)] + [Order(50)] + public bool HideDefaultPulltimer = false; + + [Checkbox("Use Regional Number Format", help = "When enabled, HSUI will use your system's regional format settings when showing numbers.\nWhen disabled, HSUI will use English number formatting instead.", separator = true)] + [Order(60)] + public bool UseRegionalNumberFormats = true; + + public new static HUDOptionsConfig DefaultConfig() => new(); + } + + public class HUDOptionsConfigConverter : PluginConfigObjectConverter + { + public HUDOptionsConfigConverter() + { + Func func = (value) => + { + Vector2[] array = new Vector2[4]; + for (int i = 0; i < 4; i++) + { + array[i] = value; + } + + return array; + }; + + TypeToClassFieldConverter castBar = new TypeToClassFieldConverter( + "CastBarOriginalPositions", + new Vector2[] { Vector2.Zero, Vector2.Zero, Vector2.Zero, Vector2.Zero }, + func + ); + + TypeToClassFieldConverter pullTimer = new TypeToClassFieldConverter( + "PulltimerOriginalPositions", + new Vector2[] { Vector2.Zero, Vector2.Zero, Vector2.Zero, Vector2.Zero }, + func + ); + + NewClassFieldConverter, Dictionary[]> jobGauge = + new NewClassFieldConverter, Dictionary[]>( + "JobGaugeOriginalPositions", + new Dictionary[] { new(), new(), new(), new() }, + (oldValue) => + { + Dictionary[] array = new Dictionary[4]; + for (int i = 0; i < 4; i++) + { + array[i] = oldValue; + } + + return array; + }); + + FieldConvertersMap.Add("CastBarOriginalPosition", castBar); + FieldConvertersMap.Add("PulltimerOriginalPosition", pullTimer); + FieldConvertersMap.Add("JobGaugeOriginalPosition", jobGauge); + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(HUDOptionsConfig); + } + } +} diff --git a/Interface/GeneralElements/HotbarsConfig.cs b/Interface/GeneralElements/HotbarsConfig.cs new file mode 100644 index 0000000..7cf37db --- /dev/null +++ b/Interface/GeneralElements/HotbarsConfig.cs @@ -0,0 +1,215 @@ +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Enums; +using HSUI.Interface; +using System.Numerics; +using Dalamud.Bindings.ImGui; + +namespace HSUI.Interface.GeneralElements +{ + /// + /// Config for a single hotbar (1-10). HotbarIndex is set by the containing config. + /// + public class HotbarBarConfig : AnchorablePluginConfigObject + { + internal int HotbarIndex { get; set; } = 1; + + [RadioSelector("12×1", "6×2", "4×3", "3×4", "2×6", "1×12", Label = "Bar Layout")] + [Order(20)] + public int BarLayout = 0; // 0=12x1, 1=6x2, 2=4x3, 3=3x4, 4=2x6, 5=1x12 + + /// Get (columns, rows) for the current BarLayout. + public (int Cols, int Rows) GetLayoutGrid() => BarLayout switch + { + 0 => (12, 1), + 1 => (6, 2), + 2 => (4, 3), + 3 => (3, 4), + 4 => (2, 6), + 5 => (1, 12), + _ => (12, 1) + }; + + [DragInt("Slot Count", min = 1, max = 12)] + [Order(21)] + public int SlotCount = 12; + + [DragInt2("Slot Size", min = 24, max = 96)] + [Order(22)] + public Vector2 SlotSize = new Vector2(40, 40); + + [DragInt("Slot Padding", min = 0, max = 16)] + [Order(23)] + public int SlotPadding = 2; + + [Checkbox("Show Cooldown Overlay")] + [Order(24)] + public bool ShowCooldownOverlay = true; + + [Checkbox("Show Cooldown Numbers")] + [Order(25)] + public bool ShowCooldownNumbers = true; + + [Checkbox("Show Border")] + [Order(26)] + public bool ShowBorder = true; + + [Checkbox("Show Tooltips")] + [Order(27)] + public bool ShowTooltips = true; + + [Checkbox("Show Keybinds")] + [Order(28)] + public bool ShowSlotNumbers = true; + + [Checkbox("Show Combo Highlight")] + [Order(29)] + public bool ShowComboHighlight = true; + + [Checkbox("Debug Drag & Drop")] + [Order(30)] + public bool DebugDragDrop = false; + + [NestedConfig("Visibility", 70)] + public VisibilityConfig VisibilityConfig = new VisibilityConfig(); + + public static HotbarBarConfig DefaultConfig(int hotbarIndex) + { + var config = new HotbarBarConfig { HotbarIndex = hotbarIndex }; + ApplyDefaults(config, hotbarIndex); + return config; + } + + protected static void ApplyDefaults(HotbarBarConfig config, int hotbarIndex) + { + var viewport = ImGui.GetMainViewport().Size; + float yOffset = 60 + (hotbarIndex - 1) * 50; + config.Position = new Vector2(0, -viewport.Y * 0.5f + yOffset); + config.Anchor = DrawAnchor.Top; + config.Size = new Vector2(12 * 40 + 11 * 2, 40); + config.HotbarIndex = hotbarIndex; + } + } + + [Exportable(false)] + [Section("Hotbars", true)] + [SubSection("General", 0)] + public class HotbarsConfig : PluginConfigObject + { + [Checkbox("Use HSUI Hotbars", help = "When enabled, HSUI hotbars replace the default game hotbars.")] + [Order(1, collapseWith = null)] + public new bool Enabled = true; + + [NestedConfig("Drag & Drop Options", 5, separator = true, collapseWith = null)] + public HotbarsGeneralOptionsConfig GeneralOptions = new(); + + public new static HotbarsConfig DefaultConfig() => new HotbarsConfig(); + } + + public class HotbarsGeneralOptionsConfig : PluginConfigObject + { + [Checkbox("Enable drag and drop from game UI", help = "When enabled, you can drag actions, macros, and items from the Actions menu, Macro menu, and Inventory onto HSUI hotbars.")] + [Order(1)] + public bool EnableDragDropFromGame = true; + + [Checkbox("Enable Shift+drag to rearrange", help = "When enabled, holding Shift and dragging a hotbar slot lets you swap it with another slot or rearrange your hotbar.")] + [Order(2)] + public bool EnableShiftDragToRearrange = true; + + [Checkbox("Enable release outside to clear slot", help = "When enabled, releasing a picked-up icon outside of HSUI hotbars clears that slot.")] + [Order(3)] + public bool EnableReleaseOutsideToClear = true; + + public new static HotbarsGeneralOptionsConfig DefaultConfig() => new(); + } + + [Exportable(false)] + [Section("Hotbars", true)] + [SubSection("Hotbar 1", 0)] + public class Hotbar1BarConfig : HotbarBarConfig + { + public Hotbar1BarConfig() => HotbarIndex = 1; + public new static Hotbar1BarConfig DefaultConfig() { var c = new Hotbar1BarConfig(); ApplyDefaults(c, 1); return c; } + } + + [Exportable(false)] + [Section("Hotbars", true)] + [SubSection("Hotbar 2", 0)] + public class Hotbar2BarConfig : HotbarBarConfig + { + public Hotbar2BarConfig() => HotbarIndex = 2; + public new static Hotbar2BarConfig DefaultConfig() { var c = new Hotbar2BarConfig(); ApplyDefaults(c, 2); return c; } + } + + [Exportable(false)] + [Section("Hotbars", true)] + [SubSection("Hotbar 3", 0)] + public class Hotbar3BarConfig : HotbarBarConfig + { + public Hotbar3BarConfig() => HotbarIndex = 3; + public new static Hotbar3BarConfig DefaultConfig() { var c = new Hotbar3BarConfig(); ApplyDefaults(c, 3); return c; } + } + + [Exportable(false)] + [Section("Hotbars", true)] + [SubSection("Hotbar 4", 0)] + public class Hotbar4BarConfig : HotbarBarConfig + { + public Hotbar4BarConfig() => HotbarIndex = 4; + public new static Hotbar4BarConfig DefaultConfig() { var c = new Hotbar4BarConfig(); ApplyDefaults(c, 4); return c; } + } + + [Exportable(false)] + [Section("Hotbars", true)] + [SubSection("Hotbar 5", 0)] + public class Hotbar5BarConfig : HotbarBarConfig + { + public Hotbar5BarConfig() => HotbarIndex = 5; + public new static Hotbar5BarConfig DefaultConfig() { var c = new Hotbar5BarConfig(); ApplyDefaults(c, 5); return c; } + } + + [Exportable(false)] + [Section("Hotbars", true)] + [SubSection("Hotbar 6", 0)] + public class Hotbar6BarConfig : HotbarBarConfig + { + public Hotbar6BarConfig() => HotbarIndex = 6; + public new static Hotbar6BarConfig DefaultConfig() { var c = new Hotbar6BarConfig(); ApplyDefaults(c, 6); return c; } + } + + [Exportable(false)] + [Section("Hotbars", true)] + [SubSection("Hotbar 7", 0)] + public class Hotbar7BarConfig : HotbarBarConfig + { + public Hotbar7BarConfig() => HotbarIndex = 7; + public new static Hotbar7BarConfig DefaultConfig() { var c = new Hotbar7BarConfig(); ApplyDefaults(c, 7); return c; } + } + + [Exportable(false)] + [Section("Hotbars", true)] + [SubSection("Hotbar 8", 0)] + public class Hotbar8BarConfig : HotbarBarConfig + { + public Hotbar8BarConfig() => HotbarIndex = 8; + public new static Hotbar8BarConfig DefaultConfig() { var c = new Hotbar8BarConfig(); ApplyDefaults(c, 8); return c; } + } + + [Exportable(false)] + [Section("Hotbars", true)] + [SubSection("Hotbar 9", 0)] + public class Hotbar9BarConfig : HotbarBarConfig + { + public Hotbar9BarConfig() => HotbarIndex = 9; + public new static Hotbar9BarConfig DefaultConfig() { var c = new Hotbar9BarConfig(); ApplyDefaults(c, 9); return c; } + } + + [Exportable(false)] + [Section("Hotbars", true)] + [SubSection("Hotbar 10", 0)] + public class Hotbar10BarConfig : HotbarBarConfig + { + public Hotbar10BarConfig() => HotbarIndex = 10; + public new static Hotbar10BarConfig DefaultConfig() { var c = new Hotbar10BarConfig(); ApplyDefaults(c, 10); return c; } + } +} diff --git a/Interface/GeneralElements/HotbarsVisibilityConfig.cs b/Interface/GeneralElements/HotbarsVisibilityConfig.cs new file mode 100644 index 0000000..c4f6791 --- /dev/null +++ b/Interface/GeneralElements/HotbarsVisibilityConfig.cs @@ -0,0 +1,71 @@ +using HSUI.Config; +using HSUI.Config.Attributes; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HSUI.Interface.GeneralElements +{ + [Disableable(false)] + [Exportable(false)] + [Section("Visibility")] + [SubSection("Hotbars", 0)] + public class HotbarsVisibilityConfig : PluginConfigObject + { + public new static HotbarsVisibilityConfig DefaultConfig() { return new HotbarsVisibilityConfig(); } + + [NestedConfig("Hotbar 1", 50)] + public VisibilityConfig HotbarConfig1 = new VisibilityConfig(); + + [NestedConfig("Hotbar 2", 51)] + public VisibilityConfig HotbarConfig2 = new VisibilityConfig(); + + [NestedConfig("Hotbar 3", 52)] + public VisibilityConfig HotbarConfig3 = new VisibilityConfig(); + + [NestedConfig("Hotbar 4", 53)] + public VisibilityConfig HotbarConfig4 = new VisibilityConfig(); + + [NestedConfig("Hotbar 5", 54)] + public VisibilityConfig HotbarConfig5 = new VisibilityConfig(); + + [NestedConfig("Hotbar 6", 55)] + public VisibilityConfig HotbarConfig6 = new VisibilityConfig(); + + [NestedConfig("Hotbar 7", 56)] + public VisibilityConfig HotbarConfig7 = new VisibilityConfig(); + + [NestedConfig("Hotbar 8", 57)] + public VisibilityConfig HotbarConfig8 = new VisibilityConfig(); + + [NestedConfig("Hotbar 9", 58)] + public VisibilityConfig HotbarConfig9 = new VisibilityConfig(); + + [NestedConfig("Hotbar 10", 59)] + public VisibilityConfig HotbarConfig10 = new VisibilityConfig(); + + [NestedConfig("Cross Hotbar", 60)] + public VisibilityConfig HotbarConfigCross = new VisibilityConfig(); + + private List _configs; + public List GetHotbarConfigs() => _configs; + + public HotbarsVisibilityConfig() + { + _configs = new List() { + HotbarConfig1, + HotbarConfig2, + HotbarConfig3, + HotbarConfig4, + HotbarConfig5, + HotbarConfig6, + HotbarConfig7, + HotbarConfig8, + HotbarConfig9, + HotbarConfig10 + }; + } + } +} diff --git a/Interface/GeneralElements/IconConfig.cs b/Interface/GeneralElements/IconConfig.cs new file mode 100644 index 0000000..6cb6632 --- /dev/null +++ b/Interface/GeneralElements/IconConfig.cs @@ -0,0 +1,172 @@ +using Dalamud.Game.ClientState.Objects.Types; +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Enums; +using HSUI.Helpers; +using HSUI.Interface.Party; +using System; +using System.Numerics; + +namespace HSUI.Interface.GeneralElements +{ + public class IconConfig : AnchorablePluginConfigObject + { + [Anchor("Frame Anchor")] + [Order(16)] + public DrawAnchor FrameAnchor = DrawAnchor.Center; + + // don't remove (used by json converter) + public IconConfig() + { + Strata = StrataLevel.MID_HIGH; + } + + public IconConfig(Vector2 position, Vector2 size, DrawAnchor anchor, DrawAnchor frameAnchor) + { + Position = position; + Size = size; + Anchor = anchor; + FrameAnchor = frameAnchor; + + Strata = StrataLevel.MID_HIGH; + } + } + + public class IconWithLabelConfig : IconConfig + { + [NestedConfig("Label", 20)] + public NumericLabelConfig NumericLabel = new NumericLabelConfig(Vector2.Zero, "", DrawAnchor.Center, DrawAnchor.Center); + + public IconWithLabelConfig(Vector2 position, Vector2 size, DrawAnchor anchor, DrawAnchor frameAnchor) + : base(position, size, anchor, frameAnchor) + { + } + } + + public class RoleJobIconConfig : IconConfig + { + public RoleJobIconConfig() : base() { } + + public RoleJobIconConfig(Vector2 position, Vector2 size, DrawAnchor anchor, DrawAnchor frameAnchor) + : base(position, size, anchor, frameAnchor) + { + } + + [Combo("Style", "Style 1", "Style 2", "Style 3", spacing = true)] + [Order(25)] + public int Style = 0; + + [Checkbox("Use Role Icons", spacing = true)] + [Order(30)] + public bool UseRoleIcons = false; + + [Checkbox("Use Specific DPS Role Icons")] + [Order(35, collapseWith = nameof(UseRoleIcons))] + public bool UseSpecificDPSRoleIcons = false; + } + + public class SignIconConfig : IconConfig + { + public SignIconConfig() : base() { } + + public SignIconConfig(Vector2 position, Vector2 size, DrawAnchor anchor, DrawAnchor frameAnchor) + : base(position, size, anchor, frameAnchor) + { + } + + [Checkbox("Preview")] + [Order(35)] + public bool Preview = false; + + public uint? IconID(IGameObject? actor) + { + if (Preview) + { + return 61231; + } + + return Utils.SignIconIDForActor(actor); + } + } + + public class NameplateIconConfig : IconConfig + { + public NameplateIconConfig() : base() { } + + public NameplateIconConfig(Vector2 position, Vector2 size, DrawAnchor anchor, DrawAnchor frameAnchor) + : base(position, size, anchor, frameAnchor) + { + } + + [Combo("Nameplate Label Anchor", new string[] { "Name", "Title", "Highest", "Lowest" }, spacing = true)] + [Order(17)] + public NameplateLabelAnchor NameplateLabelAnchor = NameplateLabelAnchor.Name; + + [Checkbox("Prioritize Health Bar as Anchor when visible", help = "When enabled, the icon will anchor to the Health Bar if it's visible.\nIf the Health Bar disappears, it will anchor back to the desired label.")] + [Order(18)] + public bool PrioritizeHealthBarAnchor = false; + } + + public class NameplatePlayerIconConfig : NameplateIconConfig + { + public NameplatePlayerIconConfig() : base() { } + + public NameplatePlayerIconConfig(Vector2 position, Vector2 size, DrawAnchor anchor, DrawAnchor frameAnchor) + : base(position, size, anchor, frameAnchor) + { + } + + [Checkbox("Only show disconnected icon", spacing = true)] + [Order(19)] + public bool OnlyShowDisconnected = false; + + public bool ShouldDrawIcon(int iconId) + { + if (!OnlyShowDisconnected) { return true; } + + return (iconId >= 61503 && iconId <= 61505) || + (iconId >= 61553 && iconId <= 61555); + } + } + + public class NameplateRoleJobIconConfig : RoleJobIconConfig + { + public NameplateRoleJobIconConfig() : base() { } + + public NameplateRoleJobIconConfig(Vector2 position, Vector2 size, DrawAnchor anchor, DrawAnchor frameAnchor) + : base(position, size, anchor, frameAnchor) + { + } + + [Combo("Nameplate Label Anchor", new string[] { "Name", "Title", "Highest", "Lowest" }, spacing = true)] + [Order(17)] + public NameplateLabelAnchor NameplateLabelAnchor = NameplateLabelAnchor.Name; + + [Checkbox("Prioritize Health Bar as Anchor when visible", help = "When enabled, the icon will anchor to the Health Bar if it's visible.\nIf the Health Bar disappears, it will anchor back to the desired label.")] + [Order(18)] + public bool PrioritizeHealthBarAnchor = false; + } + + public class PartyFramesIconsConverter : PluginConfigObjectConverter + { + public PartyFramesIconsConverter() + { + SameTypeFieldConverter converter = new SameTypeFieldConverter("FrameAnchor", DrawAnchor.Center); + FieldConvertersMap.Add("HealthBarAnchor", converter); + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(PartyFramesRoleIconConfig) || + objectType == typeof(PartyFramesLeaderIconConfig); + } + } + + public enum NameplateLabelAnchor + { + Name = 0, + Title = 1, + Highest = 2, + Lowest = 3 + } +} diff --git a/Interface/GeneralElements/LabelConfig.cs b/Interface/GeneralElements/LabelConfig.cs new file mode 100644 index 0000000..f72def1 --- /dev/null +++ b/Interface/GeneralElements/LabelConfig.cs @@ -0,0 +1,227 @@ +using Dalamud.Interface; +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Enums; +using Newtonsoft.Json; +using System; +using System.Globalization; +using System.Numerics; + +namespace HSUI.Interface.GeneralElements +{ + [Exportable(false)] + public class EditableLabelConfig : LabelConfig + { + [InputText("Text")] + [Order(10)] + public string Text; + + public EditableLabelConfig(Vector2 position, string text, DrawAnchor frameAnchor, DrawAnchor textAnchor) + : base(position, text, frameAnchor, textAnchor) + { + Text = text; + } + + public override string GetText() => Text; + + public override void SetText(string text) + { + Text = text; + } + } + + [Exportable(false)] + public class EditableNonFormattableLabelConfig : LabelConfig + { + [InputText("Text", formattable = false)] + [Order(10)] + public string Text; + + public EditableNonFormattableLabelConfig(Vector2 position, string text, DrawAnchor frameAnchor, DrawAnchor textAnchor) + : base(position, text, frameAnchor, textAnchor) + { + Text = text; + } + + public override string GetText() => Text; + + public override void SetText(string text) + { + Text = text; + } + } + + [Exportable(false)] + public class NumericLabelConfig : LabelConfig + { + [Combo("Number Format", "No Decimals (i.e. \"12\")", "One Decimal (i.e. \"12.3\")", "Two Decimals (i.e. \"12.34\")")] + [Order(10)] + public int NumberFormat; + + [Combo("Rounding Mode", "Truncate", "Floor", "Ceil", "Round")] + [Order(15)] + public int NumberFunction; + + [Checkbox("Hide Text When Zero")] + [Order(65)] + public bool HideIfZero = false; + + public NumericLabelConfig(Vector2 position, string text, DrawAnchor frameAnchor, DrawAnchor textAnchor) + : base(position, text, frameAnchor, textAnchor) + { + } + + public void SetValue(float value) + { + if (value == 0) + { + _text = HideIfZero ? string.Empty : "0"; + return; + } + + int aux = (int)Math.Pow(10, NumberFormat); + double textValue = value * aux; + + textValue = NumberFunction switch + { + 0 => Math.Truncate(textValue), + 1 => Math.Floor(textValue), + 2 => Math.Ceiling(textValue), + 3 => Math.Round(textValue), + var _ => Math.Truncate(textValue) + }; + + double v = textValue / aux; + _text = v.ToString($"F{NumberFormat}", ConfigurationManager.Instance.ActiveCultreInfo); + } + + public override NumericLabelConfig Clone(int index) => + new NumericLabelConfig(Position, _text, FrameAnchor, TextAnchor) + { + Color = Color, + OutlineColor = OutlineColor, + ShadowConfig = ShadowConfig, + ShowOutline = ShowOutline, + FontID = FontID, + UseJobColor = UseJobColor, + Enabled = Enabled, + HideIfZero = HideIfZero, + ID = ID + "_{index}" + }; + } + + [DisableParentSettings("FontID")] + [Exportable(false)] + public class IconLabelConfig : LabelConfig + { + [DragFloat("Scale", min = 1, max = 5, velocity = 0.05f)] + [Order(11)] + public float FontScale = 1; + + public FontAwesomeIcon IconId; + + public IconLabelConfig(Vector2 position, FontAwesomeIcon iconId, DrawAnchor frameAnchor, DrawAnchor textAnchor) : base(position, "", frameAnchor, textAnchor) + { + IconId = iconId; + } + + public override string GetText() => IconId.ToIconString(); + public override float GetFontScale() => FontScale; + } + + [DisableParentSettings("FontID")] + [Exportable(false)] + public class DefaultFontLabelConfig : LabelConfig + { + [DragFloat("Scale", min = 1, max = 5, velocity = 0.05f)] + [Order(11)] + public float FontScale = 1; + + public DefaultFontLabelConfig(Vector2 position, string text, DrawAnchor frameAnchor, DrawAnchor textAnchor) + : base(position, text, frameAnchor, textAnchor) + { + } + + public override bool UseSystemFont() => true; + public override float GetFontScale() => FontScale; + } + + [Exportable(false)] + public class LabelConfig : MovablePluginConfigObject + { + [JsonIgnore] protected string _text; + + [Font] + [Order(15)] + public string? FontID = null; + + [Anchor("Frame Anchor")] + [Order(20)] + public DrawAnchor FrameAnchor = DrawAnchor.Center; + + [Anchor("Text Anchor")] + [Order(25)] + public DrawAnchor TextAnchor = DrawAnchor.TopLeft; + + [ColorEdit4("Color ##Text")] + [Order(30)] + public PluginConfigColor Color = new PluginConfigColor(Vector4.One); + + [Checkbox("Outline")] + [Order(35)] + public bool ShowOutline = true; + + [ColorEdit4("Color ##Outline")] + [Order(40, collapseWith = nameof(ShowOutline))] + public PluginConfigColor OutlineColor = new PluginConfigColor(Vector4.UnitW); + + [NestedConfig("Shadow", 45)] + public ShadowConfig ShadowConfig = new ShadowConfig() { Enabled = false }; + + [Checkbox("Use Job Color", spacing = true)] + [Order(60)] + public bool UseJobColor = false; + + [Checkbox("Use Role Color")] + [Order(65)] + public bool UseRoleColor = false; + + public LabelConfig(Vector2 position, string text, DrawAnchor frameAnchor, DrawAnchor textAnchor) + { + Position = position; + _text = text; + FrameAnchor = frameAnchor; + TextAnchor = textAnchor; + Position = position; + + Strata = StrataLevel.HIGHEST; + } + + public virtual string GetText() => _text; + + public virtual void SetText(string text) + { + _text = text; + } + + public virtual PluginConfigColor GetColor() => Color; + + public virtual PluginConfigColor GetOutlineColor() => OutlineColor; + + public virtual bool UseSystemFont() => false; + public virtual float GetFontScale() => 1; + + public virtual LabelConfig Clone(int index) => + new LabelConfig(Position, _text, FrameAnchor, TextAnchor) + { + Color = Color, + OutlineColor = OutlineColor, + ShadowConfig = ShadowConfig, + ShowOutline = ShowOutline, + FontID = FontID, + UseJobColor = UseJobColor, + Enabled = Enabled, + ID = ID + "_{index}" + }; + } +} diff --git a/Interface/GeneralElements/LabelHud.cs b/Interface/GeneralElements/LabelHud.cs new file mode 100644 index 0000000..0730de5 --- /dev/null +++ b/Interface/GeneralElements/LabelHud.cs @@ -0,0 +1,258 @@ +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Interface; +using HSUI.Config; +using HSUI.Helpers; +using Dalamud.Bindings.ImGui; +using Lumina.Excel.Sheets; +using System; +using System.Numerics; + +namespace HSUI.Interface.GeneralElements +{ + public class LabelHud : HudElement + { + private LabelConfig Config => (LabelConfig)_config; + + public LabelHud(LabelConfig config) : base(config) + { + } + + protected override void CreateDrawActions(Vector2 origin) + { + // unused + } + + public override void Draw(Vector2 origin) + { + Draw(origin); + } + + public virtual void Draw( + Vector2 origin, + Vector2? parentSize = null, + IGameObject? actor = null, + string? actorName = null, + uint? actorCurrentHp = null, + uint? actorMaxHp = null, + bool? isPlayerName = null, + string? title = null) + { + if (!Config.Enabled || Config.GetText() == null) + { + return; + } + + string? text = actor == null && actorName == null && actorCurrentHp == null && actorMaxHp == null && title == null ? + Config.GetText() : + TextTagsHelper.FormattedText(Config.GetText(), actor, actorName, actorCurrentHp, actorMaxHp, isPlayerName, title); + + DrawLabel(text, origin, parentSize ?? Vector2.Zero, actor); + } + + protected virtual void DrawLabel(string text, Vector2 parentPos, Vector2 parentSize, IGameObject? actor = null) + { + Vector2 size; + Vector2 pos; + + if (Config.UseSystemFont()) + { + ImGui.PushFont(UiBuilder.DefaultFont); + size = ImGui.CalcTextSize(text) * Config.GetFontScale(); + pos = Utils.GetAnchoredPosition(Utils.GetAnchoredPosition(parentPos + Config.Position, -parentSize, Config.FrameAnchor), size, Config.TextAnchor); + ImGui.PopFont(); + } + else + { + using (FontsManager.Instance.PushFont(Config.FontID)) + { + size = ImGui.CalcTextSize(text) * Config.GetFontScale(); + pos = Utils.GetAnchoredPosition(Utils.GetAnchoredPosition(parentPos + Config.Position, -parentSize, Config.FrameAnchor), size, Config.TextAnchor); + } + } + + DrawLabel(text, pos, size, Color(actor)); + } + + public void DrawLabel(string text, Vector2 pos, Vector2 size, PluginConfigColor color, float? alpha = null) + { + if (!Config.Enabled) { return; } + + PluginConfigColor fillColor = color; + PluginConfigColor shadowColor = Config.ShadowConfig.Color; + PluginConfigColor outlineColor = Config.GetOutlineColor(); + + if (alpha.HasValue) + { + fillColor = fillColor.WithAlpha(alpha.Value); + shadowColor = shadowColor.WithAlpha(alpha.Value); + outlineColor = outlineColor.WithAlpha(alpha.Value); + } + + Action action = (ImDrawListPtr drawList) => + { + if (Config.ShadowConfig.Enabled) + { + DrawHelper.DrawShadowText(text, pos, fillColor.Base, shadowColor.Base, drawList, Config.ShadowConfig.Offset, Config.ShadowConfig.Thickness); + } + + if (Config.ShowOutline) + { + DrawHelper.DrawOutlinedText(text, pos, fillColor.Base, outlineColor.Base, drawList); + } + + if (!Config.ShowOutline && !Config.ShadowConfig.Enabled) + { + drawList.AddText(pos, fillColor.Base, text); + } + }; + + DrawHelper.DrawInWindow(ID, pos, size, false, (drawList) => + { + if (Config.UseSystemFont()) + { + ImGui.SetWindowFontScale(Config.GetFontScale()); + ImGui.PushFont(UiBuilder.DefaultFont); + action(drawList); + ImGui.PopFont(); + ImGui.SetWindowFontScale(1); + } + else + { + using (FontsManager.Instance.PushFont(Config.FontID)) + { + action(drawList); + } + } + }); + } + + public virtual PluginConfigColor Color(IGameObject? actor = null) + { + switch (Config.UseJobColor) + { + case true when (actor is ICharacter || actor is IBattleNpc battleNpc && battleNpc.ClassJob.RowId > 0): + return ColorUtils.ColorForActor(actor); + case true when actor is not ICharacter: + return GlobalColors.Instance.NPCFriendlyColor; + } + + switch (Config.UseRoleColor) + { + case true when (actor is ICharacter || actor is IBattleNpc battleNpc && battleNpc.ClassJob.RowId > 0): + { + ICharacter? character = actor as ICharacter; + return character != null && character.ClassJob.RowId > 0 ? + GlobalColors.Instance.SafeRoleColorForJobId(character.ClassJob.RowId) : + ColorUtils.ColorForActor(character); + } + case true when actor is not ICharacter: + return GlobalColors.Instance.NPCFriendlyColor; + default: + return Config.GetColor(); + } + } + + public virtual (string, Vector2, Vector2, PluginConfigColor) PreCalculate( + Vector2 origin, + Vector2? parentSize = null, + IGameObject? actor = null, + string? actorName = null, + uint? actorCurrentHp = null, + uint? actorMaxHp = null, + bool? isPlayerName = null, + string? title = null) + { + if (!Config.Enabled || Config.GetText() == null) + { + return ("", Vector2.Zero, Vector2.Zero, Color(null)); + } + + string? text = actor == null && actorName == null && actorCurrentHp == null && actorMaxHp == null && title == null ? + Config.GetText() : + TextTagsHelper.FormattedText(Config.GetText(), actor, actorName, actorCurrentHp, actorMaxHp, isPlayerName, title); + + Vector2 pSize = parentSize ?? Vector2.Zero; + Vector2 size; + Vector2 pos; + + if (Config.UseSystemFont()) + { + ImGui.PushFont(UiBuilder.DefaultFont); + size = ImGui.CalcTextSize(text) * Config.GetFontScale(); + pos = Utils.GetAnchoredPosition(Utils.GetAnchoredPosition(origin + Config.Position, -pSize, Config.FrameAnchor), size, Config.TextAnchor); + ImGui.PopFont(); + } + else + { + using (FontsManager.Instance.PushFont(Config.FontID)) + { + size = ImGui.CalcTextSize(text) * Config.GetFontScale(); + pos = Utils.GetAnchoredPosition(Utils.GetAnchoredPosition(origin + Config.Position, -pSize, Config.FrameAnchor), size, Config.TextAnchor); + } + } + + return (text, pos, size, Color(actor)); + } + } + + public class IconLabelHud : LabelHud + { + private IconLabelConfig Config => (IconLabelConfig)_config; + + public IconLabelHud(IconLabelConfig config) : base(config) + { + } + + public override void Draw(Vector2 origin, + Vector2? parentSize = null, + IGameObject? actor = null, + string? actorName = null, + uint? actorCurrentHp = null, + uint? actorMaxHp = null, + bool? isPlayerName = null, + string? title = null) + { + string? text = Config.GetText(); + if (!Config.Enabled || text == null) + { + return; + } + + DrawLabel(text, origin, parentSize ?? Vector2.Zero, actor); + } + + protected override void DrawLabel(string text, Vector2 parentPos, Vector2 parentSize, IGameObject? actor = null) + { + ImGui.PushFont(UiBuilder.IconFont); + Vector2 size = ImGui.CalcTextSize(text) * Config.GetFontScale(); + Vector2 pos = Utils.GetAnchoredPosition(Utils.GetAnchoredPosition(parentPos + Config.Position, -parentSize, Config.FrameAnchor), size, Config.TextAnchor); + ImGui.PopFont(); + + DrawHelper.DrawInWindow(ID, pos, size, false, (drawList) => + { + ImGui.SetWindowFontScale(Config.GetFontScale()); + ImGui.PushFont(UiBuilder.IconFont); + + PluginConfigColor? color = Color(actor); + + if (Config.ShadowConfig.Enabled) + { + DrawHelper.DrawShadowText(text, pos, color.Base, Config.ShadowConfig.Color.Base, drawList, Config.ShadowConfig.Offset, Config.ShadowConfig.Thickness); + } + + if (Config.ShowOutline) + { + DrawHelper.DrawOutlinedText(text, pos, color.Base, Config.OutlineColor.Base, drawList); + } + + if (!Config.ShowOutline && !Config.ShadowConfig.Enabled) + { + drawList.AddText(pos, color.Base, text); + } + + ImGui.PopFont(); + ImGui.SetWindowFontScale(1); + }); + } + } +} \ No newline at end of file diff --git a/Interface/GeneralElements/LimitBreakConfig.cs b/Interface/GeneralElements/LimitBreakConfig.cs new file mode 100644 index 0000000..fadba19 --- /dev/null +++ b/Interface/GeneralElements/LimitBreakConfig.cs @@ -0,0 +1,35 @@ +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Enums; +using HSUI.Interface.Bars; +using Dalamud.Bindings.ImGui; +using System.Numerics; + +namespace HSUI.Interface.GeneralElements +{ + [Section("Other Elements")] + [SubSection("Limit Break", 0)] + public class LimitBreakConfig : ChunkedProgressBarConfig + { + [NestedConfig("Visibility", 70)] + public VisibilityConfig VisibilityConfig = new VisibilityConfig(); + + public LimitBreakConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) : base(position, size, fillColor) + { + } + + public new static LimitBreakConfig DefaultConfig() + { + var config = new LimitBreakConfig( + new Vector2(0, -ImGui.GetMainViewport().Size.Y * 0.4f), + new Vector2(500, 10), + new PluginConfigColor(new Vector4(255f / 255f, 255f / 255f, 0f / 255f, 100f / 100f))); + + config.HideWhenInactive = true; + config.UsePartialFillColor = true; + config.PartialFillColor = new PluginConfigColor(new Vector4(0f / 255f, 181f / 255f, 255f / 255f, 100f / 100f)); + + return config; + } + } +} diff --git a/Interface/GeneralElements/LimitBreakHud.cs b/Interface/GeneralElements/LimitBreakHud.cs new file mode 100644 index 0000000..200d963 --- /dev/null +++ b/Interface/GeneralElements/LimitBreakHud.cs @@ -0,0 +1,67 @@ +using System; +using Dalamud.Game.ClientState.Objects.Types; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using FFXIVClientStructs.FFXIV.Client.Game.UI; +using FFXIVClientStructs.FFXIV.Client.UI; + +namespace HSUI.Interface.GeneralElements +{ + public class LimitBreakHud : DraggableHudElement, IHudElementWithActor, IHudElementWithVisibilityConfig + { + private LimitBreakConfig Config => (LimitBreakConfig)_config; + public VisibilityConfig VisibilityConfig => Config.VisibilityConfig; + + public IGameObject? Actor { get; set; } = null; + + public LimitBreakHud(LimitBreakConfig config, string displayName) : base(config, displayName) { } + + protected override (List, List) ChildrenPositionsAndSizes() + { + return (new List() { Config.Position }, new List() { Config.Size }); + } + + public override unsafe void DrawChildren(Vector2 origin) + { + Config.Label.SetText(""); + + if (!Config.Enabled) + { + return; + } + + LimitBreakController* lbController = LimitBreakController.Instance(); + AddonHWDAetherGauge* caGauge = (AddonHWDAetherGauge*) Plugin.GameGui.GetAddonByName("HWDAetherGauge", 1).Address; + + int currentLimitBreak = lbController->CurrentUnits; + int maxLimitBreak = lbController->BarUnits * lbController->BarCount; + int limitBreakChunks = lbController->BarCount; + + if (caGauge != null) + { + currentLimitBreak = caGauge->MaxGaugeValue; + maxLimitBreak = 1000; + limitBreakChunks = 5; + } + + int valuePerChunk = limitBreakChunks == 0 ? 0 : maxLimitBreak / limitBreakChunks; + int currentChunksFilled = valuePerChunk == 0 ? 0 : currentLimitBreak / valuePerChunk; + + if (Config.HideWhenInactive && limitBreakChunks == 0) + { + return; + } + + Config.Label.SetValue(currentChunksFilled); + + BarHud[] bars = BarUtilities.GetChunkedProgressBars(Config, limitBreakChunks, currentLimitBreak, maxLimitBreak); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.StrataLevel)); + } + } + } +} diff --git a/Interface/GeneralElements/MPTickerConfig.cs b/Interface/GeneralElements/MPTickerConfig.cs new file mode 100644 index 0000000..a404571 --- /dev/null +++ b/Interface/GeneralElements/MPTickerConfig.cs @@ -0,0 +1,75 @@ +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Interface.Bars; +using System.Numerics; + +namespace HSUI.Interface.GeneralElements +{ + [DisableParentSettings("Position")] + [Section("Other Elements")] + [SubSection("MP Ticker", 0)] + public class MPTickerConfig : MovablePluginConfigObject + { + [Checkbox("Hide on Full MP", spacing = false)] + [Order(15)] + public bool HideOnFullMP = true; + + [Checkbox("Enable Only for BLM")] + [Order(20)] + public bool EnableOnlyForBLM = false; + + [Checkbox("Show Only During Umbral Ice")] + [Order(25, collapseWith = nameof(EnableOnlyForBLM))] + public bool ShowOnlyDuringUmbralIce = true; + + [NestedConfig("MP Ticker Bar", 30)] + public MPTickerBarConfig Bar = new MPTickerBarConfig( + Vector2.Zero, + new Vector2(254, 8), + new PluginConfigColor(new(240f / 255f, 92f / 255f, 232f / 255f, 100f / 100f)) + ); + + [NestedConfig("Visibility", 70)] + public VisibilityConfig VisibilityConfig = new VisibilityConfig(); + + public new static MPTickerConfig DefaultConfig() + { + var config = new MPTickerConfig(); + config.Enabled = false; + config.Bar.Position = new Vector2(0, HUDConstants.BaseHUDOffsetY + 27); + + return config; + } + } + + [Disableable(false)] + [DisableParentSettings("HideWhenInactive")] + [Exportable(false)] + public class MPTickerBarConfig : BarConfig + { + [NestedConfig("Fire III Threshold (BLM only)", 50, separator = false, spacing = true)] + public MPTickerFire3ThresholdConfig Fire3Threshold = new MPTickerFire3ThresholdConfig(); + + public MPTickerBarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) + : base(position, size, fillColor) + { + } + } + + [Exportable(false)] + [DisableParentSettings("Value")] + public class MPTickerFire3ThresholdConfig : ThresholdConfig + { + [DragFloat("Estimated Fire III Cast Time", min = 0f, max = 10)] + [Order(11)] + public float Fire3CastTime = 1.5f; + + public MPTickerFire3ThresholdConfig() + { + Enabled = false; + ThresholdType = ThresholdType.Above; + ShowMarker = true; + MarkerColor = new(new Vector4(255f / 255f, 136f / 255f, 0 / 255f, 90f / 100f)); + } + } +} diff --git a/Interface/GeneralElements/MPTickerHud.cs b/Interface/GeneralElements/MPTickerHud.cs new file mode 100644 index 0000000..28c2595 --- /dev/null +++ b/Interface/GeneralElements/MPTickerHud.cs @@ -0,0 +1,110 @@ +using HSUI.Helpers; +using Dalamud.Bindings.ImGui; +using System; +using System.Collections.Generic; +using System.Numerics; +using Dalamud.Game.ClientState.Objects.Types; +using HSUI.Interface.Bars; +using Dalamud.Game.ClientState.Objects.SubKinds; +using System.Linq; +using Dalamud.Game.ClientState.JobGauge.Types; + +namespace HSUI.Interface.GeneralElements +{ + public class MPTickerHud : DraggableHudElement, IHudElementWithActor, IHudElementWithVisibilityConfig + { + private MPTickerConfig Config => (MPTickerConfig)_config; + public VisibilityConfig VisibilityConfig => Config.VisibilityConfig; + + private MPTickHelper _mpTickHelper = null!; + public IGameObject? Actor { get; set; } = null; + + public MPTickerHud(MPTickerConfig config, string displayName) : base(config, displayName) { } + + protected override void InternalDispose() + { + _mpTickHelper?.Dispose(); + } + + protected override (List, List) ChildrenPositionsAndSizes() + { + return (new List() { Config.Position + Config.Bar.Position }, + new List() { Config.Bar.Size }); + } + + public override void DrawChildren(Vector2 origin) + { + if (!Config.Enabled || Actor == null || Actor is not IPlayerCharacter player) + { + return; + } + + // full mp + if (Config.HideOnFullMP && player.CurrentMp >= player.MaxMp) + { + return; + } + + // BLM specific settings + if (Config.EnableOnlyForBLM) + { + if (player.ClassJob.RowId != JobIDs.BLM) + { + + return; + } + else + { + var gauge = Plugin.JobGauges.Get(); + if (Config.ShowOnlyDuringUmbralIce && !gauge.InUmbralIce) + { + return; + } + } + } + + _mpTickHelper ??= new MPTickHelper(); + + var now = ImGui.GetTime(); + var scale = (float)((now - _mpTickHelper.LastTick) / MPTickHelper.ServerTickRate); + + if (scale <= 0) + { + return; + } + + if (scale > 1) + { + scale = 1; + } + + MPTickerFire3ThresholdConfig? thresholdConfig = GetFire3ThresholdConfig(); + BarHud bar = BarUtilities.GetProgressBar(Config.Bar, thresholdConfig, null, scale, 1, 0, fillColor: Config.Bar.FillColor); + + AddDrawActions(bar.GetDrawActions(origin + Config.Position, _config.StrataLevel)); + } + + private MPTickerFire3ThresholdConfig? GetFire3ThresholdConfig() + { + if (Actor is not IPlayerCharacter player || player.ClassJob.RowId != JobIDs.BLM) + { + return null; + } + + MPTickerFire3ThresholdConfig config = Config.Bar.Fire3Threshold; + if (!config.Enabled) + { + return null; + } + + bool leyLinesActive = Utils.StatusListForBattleChara(player).Any(e => e.StatusId == 738); + float castTime = config.Fire3CastTime * (leyLinesActive ? 0.85f : 1f); + + // tick rate is 3s + // adding 0.3f as "safety net" + config.Value = (3 - castTime + 0.3f) / 3; + + return config; + } + } +} diff --git a/Interface/GeneralElements/PrimaryResourceConfig.cs b/Interface/GeneralElements/PrimaryResourceConfig.cs new file mode 100644 index 0000000..bf07983 --- /dev/null +++ b/Interface/GeneralElements/PrimaryResourceConfig.cs @@ -0,0 +1,139 @@ +using HSUI.Config.Attributes; +using HSUI.Enums; +using HSUI.Interface.Bars; +using System.Numerics; + +namespace HSUI.Interface.GeneralElements +{ + [DisableParentSettings("HideWhenInactive", "Label")] + [Section("Mana Bars")] + [SubSection("Player", 0)] + public class PlayerPrimaryResourceConfig : UnitFramePrimaryResourceConfig + { + public PlayerPrimaryResourceConfig(Vector2 position, Vector2 size) + : base(position, size) + { + + } + + public new static PlayerPrimaryResourceConfig DefaultConfig() + { + var size = new Vector2(HUDConstants.DefaultBigUnitFrameSize.X, 10); + var pos = new Vector2(0, 0); + + var config = new PlayerPrimaryResourceConfig(pos, size); + config.Anchor = DrawAnchor.Bottom; + + return config; + } + } + + [DisableParentSettings("HideWhenInactive", "Label")] + [Section("Mana Bars")] + [SubSection("Target", 0)] + public class TargetPrimaryResourceConfig : UnitFramePrimaryResourceConfig + { + public TargetPrimaryResourceConfig(Vector2 position, Vector2 size) + : base(position, size) + { + + } + + public new static TargetPrimaryResourceConfig DefaultConfig() + { + var size = new Vector2(HUDConstants.DefaultBigUnitFrameSize.X, 10); + var pos = new Vector2(0, 0); + + var config = new TargetPrimaryResourceConfig(pos, size); + config.Anchor = DrawAnchor.Bottom; + + return config; + } + } + + [DisableParentSettings("HideWhenInactive", "Label")] + [Section("Mana Bars")] + [SubSection("Target of Target", 0)] + public class TargetOfTargetPrimaryResourceConfig : UnitFramePrimaryResourceConfig + { + public TargetOfTargetPrimaryResourceConfig(Vector2 position, Vector2 size) + : base(position, size) + { + + } + + public new static TargetOfTargetPrimaryResourceConfig DefaultConfig() + { + var size = new Vector2(HUDConstants.DefaultSmallUnitFrameSize.X, 10); + var pos = new Vector2(0, 0); + + var config = new TargetOfTargetPrimaryResourceConfig(pos, size); + config.Anchor = DrawAnchor.Bottom; + + return config; + } + } + + [DisableParentSettings("HideWhenInactive", "Label")] + [Section("Mana Bars")] + [SubSection("Focus Target", 0)] + public class FocusTargetPrimaryResourceConfig : UnitFramePrimaryResourceConfig + { + public FocusTargetPrimaryResourceConfig(Vector2 position, Vector2 size) + : base(position, size) + { + + } + + public new static FocusTargetPrimaryResourceConfig DefaultConfig() + { + var size = new Vector2(HUDConstants.DefaultSmallUnitFrameSize.X, 10); + var pos = new Vector2(0, 0); + + var config = new FocusTargetPrimaryResourceConfig(pos, size); + config.Anchor = DrawAnchor.Bottom; + + return config; + } + } + + public abstract class UnitFramePrimaryResourceConfig : PrimaryResourceConfig + { + [Checkbox("Anchor to Unit Frame")] + [Order(16)] + public bool AnchorToUnitFrame = true; + + [Anchor("Unit Frame Anchor")] + [Order(17, collapseWith = nameof(AnchorToUnitFrame))] + public DrawAnchor UnitFrameAnchor = DrawAnchor.Bottom; + + [NestedConfig("Visibility", 1200)] + public VisibilityConfig VisibilityConfig = new VisibilityConfig(); + + public UnitFramePrimaryResourceConfig(Vector2 position, Vector2 size) + : base(position, size) + { + + } + } + + public abstract class PrimaryResourceConfig : ProgressBarConfig + { + [Checkbox("Use Job Color", spacing = true)] + [Order(19)] + public bool UseJobColor = false; + + [Checkbox("Hide When Full", spacing = true)] + [Order(41)] + public bool HidePrimaryResourceWhenFull = false; + + [NestedConfig("Label", 1000, separator = false, spacing = true)] + public EditableLabelConfig ValueLabel = new EditableLabelConfig(Vector2.Zero, "[mana:current]", DrawAnchor.Center, DrawAnchor.Center); + + public PrimaryResourceConfig(Vector2 position, Vector2 size) + : base(position, size, new(new(0 / 255f, 162f / 255f, 252f / 255f, 100f / 100f))) + { + Strata = StrataLevel.LOW; + } + } +} \ No newline at end of file diff --git a/Interface/GeneralElements/PrimaryResourceHud.cs b/Interface/GeneralElements/PrimaryResourceHud.cs new file mode 100644 index 0000000..7883506 --- /dev/null +++ b/Interface/GeneralElements/PrimaryResourceHud.cs @@ -0,0 +1,134 @@ +using HSUI.Config; +using HSUI.Helpers; +using Dalamud.Bindings.ImGui; +using System.Collections.Generic; +using System.Numerics; +using Dalamud.Game.ClientState.Objects.Types; +using System; +using Dalamud.Game.ClientState.Objects.SubKinds; +using HSUI.Enums; +using HSUI.Interface.Bars; +using HSUI.Interface.Party; + +namespace HSUI.Interface.GeneralElements +{ + public class PrimaryResourceHud : ParentAnchoredDraggableHudElement, IHudElementWithActor, IHudElementWithAnchorableParent, IHudElementWithVisibilityConfig + { + private PrimaryResourceConfig Config => (PrimaryResourceConfig)_config; + public VisibilityConfig? VisibilityConfig => Config is UnitFramePrimaryResourceConfig config ? config.VisibilityConfig : null; + + public PrimaryResourceTypes ResourceType = PrimaryResourceTypes.MP; + + private IGameObject? _actor; + public IGameObject? Actor + { + get => _actor; + set + { + if (value is IPlayerCharacter chara) + { + _actor = value; + + JobRoles role = JobsHelper.RoleForJob(chara.ClassJob.RowId); + ResourceType = JobsHelper.PrimaryResourceTypesByRole[role]; + } + else + { + _actor = null; + ResourceType = PrimaryResourceTypes.None; + } + } + } + + public IPartyFramesMember? PartyMember; + + protected override bool AnchorToParent => Config is UnitFramePrimaryResourceConfig config ? config.AnchorToUnitFrame : false; + protected override DrawAnchor ParentAnchor => Config is UnitFramePrimaryResourceConfig config ? config.UnitFrameAnchor : DrawAnchor.Center; + + public PrimaryResourceHud(PrimaryResourceConfig config, string? displayName = null) : base(config, displayName) + { + } + + protected override (List, List) ChildrenPositionsAndSizes() + { + return (new List() { Config.Position }, new List() { Config.Size }); + } + + public override void DrawChildren(Vector2 origin) + { + if (!Config.Enabled) + { + return; + } + + if (PartyMember == null && (ResourceType == PrimaryResourceTypes.None || Actor == null || Actor is not IPlayerCharacter)) + { + return; + } + + ICharacter? chara = Actor != null ? (ICharacter)Actor : null; + uint current = chara == null ? PartyMember?.MP ?? 0 : 0; + uint max = chara == null ? PartyMember?.MaxMP ?? 0 : 0; + + if (chara != null) + { + GetResources(ref current, ref max, chara); + } + + if (Config.HidePrimaryResourceWhenFull && current == max) + { + return; + } + + BarHud bar = BarUtilities.GetProgressBar( + Config, + Config.ThresholdConfig, + new LabelConfig[] { Config.ValueLabel }, + current, + max, + 0, + chara, + GetColor() + ); + + Vector2 pos = origin + ParentPos(); + AddDrawActions(bar.GetDrawActions(pos, Config.StrataLevel)); + } + + private void GetResources(ref uint current, ref uint max, ICharacter actor) + { + switch (ResourceType) + { + case PrimaryResourceTypes.MP: + current = actor.CurrentMp; + max = actor.MaxMp; + break; + + case PrimaryResourceTypes.CP: + current = actor.CurrentCp; + max = actor.MaxCp; + break; + + case PrimaryResourceTypes.GP: + current = actor.CurrentGp; + max = actor.MaxGp; + break; + } + } + + public virtual PluginConfigColor GetColor() + { + if (!Config.UseJobColor) + { + return Config.FillColor; + } + + if (PartyMember != null) + { + return GlobalColors.Instance.SafeColorForJobId(PartyMember.JobId); + } + + return ColorUtils.ColorForActor(Actor); + } + } +} diff --git a/Interface/GeneralElements/PullTimerConfig.cs b/Interface/GeneralElements/PullTimerConfig.cs new file mode 100644 index 0000000..0d05145 --- /dev/null +++ b/Interface/GeneralElements/PullTimerConfig.cs @@ -0,0 +1,33 @@ +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Enums; +using HSUI.Interface.Bars; +using System.Numerics; + +namespace HSUI.Interface.GeneralElements +{ + [Section("Other Elements")] + [SubSection("Pull Timer", 0)] + public class PullTimerConfig : ProgressBarConfig + { + [Checkbox("Use Job Color")] + [Order(45)] + public bool UseJobColor = false; + + public PullTimerConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) : base(position, size, fillColor) + { + } + + public new static PullTimerConfig DefaultConfig() + { + var config = new PullTimerConfig( + new Vector2(0, HUDConstants.BaseHUDOffsetY - 35), + new Vector2(254, 20), + new PluginConfigColor(new Vector4(233f / 255f, 4f / 255f, 4f / 255f, 100f / 100f))); + + config.HideWhenInactive = true; + + return config; + } + } +} diff --git a/Interface/GeneralElements/PullTimerHud.cs b/Interface/GeneralElements/PullTimerHud.cs new file mode 100644 index 0000000..04326aa --- /dev/null +++ b/Interface/GeneralElements/PullTimerHud.cs @@ -0,0 +1,53 @@ +using Dalamud.Game.ClientState.Objects.Types; +using HSUI.Config; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using System.Collections.Generic; +using System.Numerics; + +namespace HSUI.Interface.GeneralElements +{ + public class PullTimerHud : DraggableHudElement, IHudElementWithActor + { + private PullTimerConfig Config => (PullTimerConfig)_config; + + public IGameObject? Actor { get; set; } = null; + + public PullTimerHud(PullTimerConfig config, string displayName) : base(config, displayName) { } + + protected override (List, List) ChildrenPositionsAndSizes() + { + return (new List() { Config.Position }, new List() { Config.Size }); + } + + public override void DrawChildren(Vector2 origin) + { + PullTimerState helper = PullTimerHelper.Instance.PullTimerState; + + Config.Label.SetValue(helper.CountDownValue); + + if (!helper.CountingDown) + { + Config.Label.SetText(""); + } + + if (!Config.Enabled || Actor is null) + { + return; + } + + if (Config.HideWhenInactive && !helper.CountingDown) + { + return; + } + + PluginConfigColor? fillColor = Config.UseJobColor ? ColorUtils.ColorForActor(Actor) : null; + + BarHud bar = BarUtilities.GetProgressBar(Config, + helper.CountDownValue, + helper.CountDownMax, 0F, Actor, fillColor); + + AddDrawActions(bar.GetDrawActions(origin, Config.StrataLevel)); + } + } +} diff --git a/Interface/GeneralElements/ShadowConfig.cs b/Interface/GeneralElements/ShadowConfig.cs new file mode 100644 index 0000000..8a582a3 --- /dev/null +++ b/Interface/GeneralElements/ShadowConfig.cs @@ -0,0 +1,22 @@ +using System.Numerics; +using HSUI.Config; +using HSUI.Config.Attributes; + +namespace HSUI.Interface.GeneralElements +{ + [Exportable(false)] + public class ShadowConfig : PluginConfigObject + { + [ColorEdit4("Color")] + [Order(5)] + public PluginConfigColor Color = new PluginConfigColor(new Vector4(0f / 255f, 0f / 255f, 0f / 255f, 100f / 100f)); + + [DragInt("Thickness", min = 1, max = 20)] + [Order(10)] + public int Thickness = 1; + + [DragInt("Offset", min = 0, max = 20)] + [Order(15)] + public int Offset = 2; + } +} diff --git a/Interface/GeneralElements/UnitFrameConfig.cs b/Interface/GeneralElements/UnitFrameConfig.cs new file mode 100644 index 0000000..e846d21 --- /dev/null +++ b/Interface/GeneralElements/UnitFrameConfig.cs @@ -0,0 +1,401 @@ +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Enums; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using System.Numerics; + +namespace HSUI.Interface.GeneralElements +{ + [DisableParentSettings("HideWhenInactive", "HideHealthIfPossible", "RangeConfig", "EnemyRangeConfig")] + [Section("Unit Frames")] + [SubSection("Player", 0)] + public class PlayerUnitFrameConfig : UnitFrameConfig + { + [NestedConfig("Tank Stance Indicator", 122, spacing = true)] + public TankStanceIndicatorConfig TankStanceIndicatorConfig = new TankStanceIndicatorConfig(); + + public PlayerUnitFrameConfig(Vector2 position, Vector2 size, EditableLabelConfig leftLabelConfig, EditableLabelConfig rightLabelConfig, EditableLabelConfig optionalLabelConfig) + : base(position, size, leftLabelConfig, rightLabelConfig, optionalLabelConfig) + { + } + + public new static PlayerUnitFrameConfig DefaultConfig() + { + var size = HUDConstants.DefaultBigUnitFrameSize; + var pos = new Vector2(-HUDConstants.UnitFramesOffsetX - size.X / 2f, HUDConstants.BaseHUDOffsetY); + + var leftLabelConfig = new EditableLabelConfig(new Vector2(5, 0), "[name]", DrawAnchor.TopLeft, DrawAnchor.BottomLeft); + var rightLabelConfig = new EditableLabelConfig(new Vector2(-5, 0), "[health:current-short] | [health:percent]", DrawAnchor.TopRight, DrawAnchor.BottomRight); + var optionalLabelConfig = new EditableLabelConfig(new Vector2(0, 0), "", DrawAnchor.Center, DrawAnchor.Center); + + var config = new PlayerUnitFrameConfig(pos, size, leftLabelConfig, rightLabelConfig, optionalLabelConfig); + + return config; + } + } + + public enum TankStanceCorner + { + TopLeft = 0, + TopRight, + BottomLeft, + BottomRight + } + + [Exportable(false)] + public class TankStanceIndicatorConfig : PluginConfigObject + { + [Combo("Corner", "Top Left", "Top Right", "Bottom Left", "Bottom Right")] + [Order(5)] + public TankStanceCorner Corner = TankStanceCorner.BottomLeft; + + [DragFloat2("Size", min = 1, max = 500)] + [Order(10)] + public Vector2 Size = new Vector2(HUDConstants.DefaultBigUnitFrameSize.Y - 20, HUDConstants.DefaultBigUnitFrameSize.Y - 20); + + [DragInt("Thickness", min = 2, max = 20)] + [Order(15)] + public int Thickess = 4; + + [ColorEdit4("Active Color")] + [Order(20)] + public PluginConfigColor ActiveColor = new PluginConfigColor(new Vector4(0f / 255f, 255f / 255f, 255f / 255f, 100f / 100f)); + + [ColorEdit4("Inactive Color")] + [Order(25)] + public PluginConfigColor InactiveColor = new PluginConfigColor(new Vector4(255f / 255f, 0f / 255f, 0f / 255f, 100f / 100f)); + } + + [DisableParentSettings("HideWhenInactive")] + [Section("Unit Frames")] + [SubSection("Target", 0)] + public class TargetUnitFrameConfig : UnitFrameConfig + { + public TargetUnitFrameConfig(Vector2 position, Vector2 size, EditableLabelConfig leftLabelConfig, EditableLabelConfig rightLabelConfig, EditableLabelConfig optionalLabelConfig) + : base(position, size, leftLabelConfig, rightLabelConfig, optionalLabelConfig) + { + } + + public new static TargetUnitFrameConfig DefaultConfig() + { + var size = HUDConstants.DefaultBigUnitFrameSize; + var pos = new Vector2(HUDConstants.UnitFramesOffsetX + size.X / 2f, HUDConstants.BaseHUDOffsetY); + + var leftLabelConfig = new EditableLabelConfig(new Vector2(5, 0), "[health:current-short] | [health:percent]", DrawAnchor.TopLeft, DrawAnchor.BottomLeft); + var rightLabelConfig = new EditableLabelConfig(new Vector2(-5, 0), "[name]", DrawAnchor.TopRight, DrawAnchor.BottomRight); + var optionalLabelConfig = new EditableLabelConfig(new Vector2(0, 0), "", DrawAnchor.Center, DrawAnchor.Center); + + return new TargetUnitFrameConfig(pos, size, leftLabelConfig, rightLabelConfig, optionalLabelConfig); + } + } + + [DisableParentSettings("HideWhenInactive")] + [Section("Unit Frames")] + [SubSection("Target of Target", 0)] + public class TargetOfTargetUnitFrameConfig : UnitFrameConfig + { + public TargetOfTargetUnitFrameConfig(Vector2 position, Vector2 size, EditableLabelConfig leftLabelConfig, EditableLabelConfig rightLabelConfig, EditableLabelConfig optionalLabelConfig) + : base(position, size, leftLabelConfig, rightLabelConfig, optionalLabelConfig) + { + } + + public new static TargetOfTargetUnitFrameConfig DefaultConfig() + { + var size = HUDConstants.DefaultSmallUnitFrameSize; + var pos = new Vector2( + HUDConstants.UnitFramesOffsetX + HUDConstants.DefaultBigUnitFrameSize.X + 6 + size.X / 2f, + HUDConstants.BaseHUDOffsetY - 15 + ); + + var leftLabelConfig = new EditableLabelConfig(new Vector2(0, 0), "[name]", DrawAnchor.Top, DrawAnchor.Bottom); + var rightLabelConfig = new EditableLabelConfig(new Vector2(0, 0), "", DrawAnchor.Center, DrawAnchor.TopLeft); + var optionalLabelConfig = new EditableLabelConfig(new Vector2(0, 0), "", DrawAnchor.Center, DrawAnchor.BottomLeft); + + return new TargetOfTargetUnitFrameConfig(pos, size, leftLabelConfig, rightLabelConfig, optionalLabelConfig); + } + } + + [DisableParentSettings("HideWhenInactive")] + [Section("Unit Frames")] + [SubSection("Focus Target", 0)] + public class FocusTargetUnitFrameConfig : UnitFrameConfig + { + public FocusTargetUnitFrameConfig(Vector2 position, Vector2 size, EditableLabelConfig leftLabelConfig, EditableLabelConfig rightLabelConfig, EditableLabelConfig optionalLabelConfig) + : base(position, size, leftLabelConfig, rightLabelConfig, optionalLabelConfig) + { + } + + public new static FocusTargetUnitFrameConfig DefaultConfig() + { + var size = HUDConstants.DefaultSmallUnitFrameSize; + var pos = new Vector2( + -HUDConstants.UnitFramesOffsetX - HUDConstants.DefaultBigUnitFrameSize.X - 6 - size.X / 2f, + HUDConstants.BaseHUDOffsetY - 15 + ); + + var leftLabelConfig = new EditableLabelConfig(new Vector2(0, 0), "[name]", DrawAnchor.Top, DrawAnchor.Bottom); + var rightLabelConfig = new EditableLabelConfig(new Vector2(0, 0), "", DrawAnchor.Center, DrawAnchor.Center); + var optionalLabelConfig = new EditableLabelConfig(new Vector2(0, 0), "", DrawAnchor.Bottom, DrawAnchor.Bottom); + + return new FocusTargetUnitFrameConfig(pos, size, leftLabelConfig, rightLabelConfig, optionalLabelConfig); + } + } + + [DisableParentSettings("HideWhenInactive")] + public class UnitFrameConfig : BarConfig + { + [Checkbox("Use Job Color", spacing = true)] + [Order(45)] + public bool UseJobColor = true; + + [Checkbox("Use Role Color")] + [Order(46)] + public bool UseRoleColor = false; + + [NestedConfig("Color Based On Health Value", 50, collapsingHeader = false)] + public ColorByHealthValueConfig ColorByHealth = new ColorByHealthValueConfig(); + + [Checkbox("Job Color As Background Color", spacing = true)] + [Order(50)] + public bool UseJobColorAsBackgroundColor = false; + + [Checkbox("Role Color As Background Color")] + [Order(51)] + public bool UseRoleColorAsBackgroundColor = false; + + [Checkbox("Missing Health Color")] + [Order(55)] + public bool UseMissingHealthBar = false; + + [Checkbox("Job Color As Missing Health Color")] + [Order(56, collapseWith = nameof(UseMissingHealthBar))] + public bool UseJobColorAsMissingHealthColor = false; + + [Checkbox("Role Color As Missing Health Color")] + [Order(57, collapseWith = nameof(UseMissingHealthBar))] + public bool UseRoleColorAsMissingHealthColor = false; + + [ColorEdit4("Color" + "##MissingHealth")] + [Order(60, collapseWith = nameof(UseMissingHealthBar))] + public PluginConfigColor HealthMissingColor = new PluginConfigColor(new Vector4(255f / 255f, 0f / 255f, 0f / 255f, 100f / 100f)); + + [Checkbox("Death Indicator Background Color", spacing = true)] + [Order(61)] + public bool UseDeathIndicatorBackgroundColor = false; + + [ColorEdit4("Color" + "##DeathIndicator")] + [Order(62, collapseWith = nameof(UseDeathIndicatorBackgroundColor))] + public PluginConfigColor DeathIndicatorBackgroundColor = new PluginConfigColor(new Vector4(204f / 255f, 3f / 255f, 3f / 255f, 50f / 100f)); + + [Checkbox("Tank Invulnerability", spacing = true)] + [Order(95)] + public bool ShowTankInvulnerability = true; + + [Checkbox("Tank Invulnerability Custom Color")] + [Order(100, collapseWith = nameof(ShowTankInvulnerability))] + public bool UseCustomInvulnerabilityColor = true; + + [ColorEdit4("Tank Invulnerability Color ##TankInvulnerabilityCustom")] + [Order(105, collapseWith = nameof(UseCustomInvulnerabilityColor))] + public PluginConfigColor CustomInvulnerabilityColor = new PluginConfigColor(new Vector4(211f / 255f, 235f / 255f, 215f / 245f, 50f / 100f)); + + [Checkbox("Walking Dead Custom Color")] + [Order(110, collapseWith = nameof(ShowTankInvulnerability))] + public bool UseCustomWalkingDeadColor = true; + + [ColorEdit4("Walking Dead Color ##TankWalkingDeadCustom")] + [Order(115, collapseWith = nameof(UseCustomWalkingDeadColor))] + public PluginConfigColor CustomWalkingDeadColor = new PluginConfigColor(new Vector4(158f / 255f, 158f / 255f, 158f / 255f, 50f / 100f)); + + [NestedConfig("Use Smooth Transitions", 120, collapsingHeader = false)] + public SmoothHealthConfig SmoothHealthConfig = new SmoothHealthConfig(); + + [Checkbox("Hide Health if Possible", spacing = true, help = "This will hide any label that has a health tag if the character doesn't have health (ie minions, friendly npcs, etc)")] + [Order(121)] + public bool HideHealthIfPossible = true; + + [NestedConfig("Left Text", 125)] + public EditableLabelConfig LeftLabelConfig = null!; + + [NestedConfig("Right Text", 130)] + public EditableLabelConfig RightLabelConfig = null!; + + [NestedConfig("Optional Text", 131)] + public EditableLabelConfig OptionalLabelConfig = null!; + + [NestedConfig("Role/Job Icon", 135)] + public RoleJobIconConfig RoleIconConfig = new RoleJobIconConfig( + new Vector2(5, 0), + new Vector2(30, 30), + DrawAnchor.Left, + DrawAnchor.Left + ); + + [NestedConfig("Sign Icon", 136)] + public SignIconConfig SignIconConfig = new SignIconConfig( + new Vector2(0, 0), + new Vector2(30, 30), + DrawAnchor.Center, + DrawAnchor.Top + ); + + [NestedConfig("Shields", 140)] + public ShieldConfig ShieldConfig = new ShieldConfig(); + + [NestedConfig("Change Friendly Alpha Based on Range", 145)] + public UnitFramesRangeConfig RangeConfig = new(); + + [NestedConfig("Change Enemy Alpha Based on Range", 146)] + public UnitFramesRangeConfig EnemyRangeConfig = new(); + + [NestedConfig("Custom Mouseover Area", 150)] + public MouseoverAreaConfig MouseoverAreaConfig = new MouseoverAreaConfig(); + + [NestedConfig("Visibility", 200)] + public VisibilityConfig VisibilityConfig = new VisibilityConfig(); + + public UnitFrameConfig(Vector2 position, Vector2 size, EditableLabelConfig leftLabelConfig, EditableLabelConfig rightLabelConfig, EditableLabelConfig optionalLabelConfig) + : base(position, size, new PluginConfigColor(new(40f / 255f, 40f / 255f, 40f / 255f, 100f / 100f))) + { + Position = position; + Size = size; + LeftLabelConfig = leftLabelConfig; + RightLabelConfig = rightLabelConfig; + OptionalLabelConfig = optionalLabelConfig; + BackgroundColor = new PluginConfigColor(new(0f / 255f, 0f / 255f, 0f / 255f, 100f / 100f)); + RoleIconConfig.Enabled = false; + SignIconConfig.Enabled = false; + ColorByHealth.Enabled = false; + MouseoverAreaConfig.Enabled = false; + } + + public UnitFrameConfig() : base(Vector2.Zero, Vector2.Zero, PluginConfigColor.Empty) { } // don't remove + } + + [Exportable(false)] + public class ShieldConfig : PluginConfigObject + { + [DragInt("Thickness")] + [Order(5)] + public int Height = 26; // Should be 'Size' instead of 'Height' but leaving as is to avoid breaking configs + + [Checkbox("Thickness in Pixels")] + [Order(10)] + public bool HeightInPixels = false; + + [Checkbox("Fill Health First")] + [Order(15)] + public bool FillHealthFirst = true; + + [ColorEdit4("Color ##Shields")] + [Order(20)] + public PluginConfigColor Color = new PluginConfigColor(new Vector4(198f / 255f, 210f / 255f, 255f / 255f, 70f / 100f)); + } + + [Exportable(false)] + public class SmoothHealthConfig : PluginConfigObject + { + [DragFloat("Velocity", min = 1f, max = 100f)] + [Order(5)] + public float Velocity = 25f; + } + + [Exportable(false)] + public class MouseoverAreaConfig : PluginConfigObject + { + [Checkbox("Preview")] + [Order(5)] + public bool Preview = false; + + [Checkbox("Ignore Mouseover", help = "Enabling this will make it so this element is ignored by mouseover completely.\nThe area can still be defined for left and right clicks.")] + [Order(6)] + public bool Ignore = false; + + [DragInt2("Top Left Offset", min = -500, max = 500)] + [Order(10)] + public Vector2 TopLeftOffset = Vector2.Zero; + + [DragInt2("Bottom Right Offset", min = -500, max = 500)] + [Order(11)] + public Vector2 BottomRightOffset = Vector2.Zero; + + public MouseoverAreaConfig() + { + Enabled = false; + } + + public (Vector2, Vector2) GetArea(Vector2 pos, Vector2 size) + { + if (!Enabled) { return (pos, pos + size); } + + Vector2 start = pos + TopLeftOffset; + Vector2 end = pos + size + BottomRightOffset; + + return (start, end); + } + + public BarHud? GetBar(Vector2 pos, Vector2 size, string id, DrawAnchor anchor = DrawAnchor.TopLeft) + { + if (!Enabled || !Preview) { return null; } + + BarHud bar = new BarHud( + id, + true, + new(Vector4.One), + 2 + ); + + var barPos = Utils.GetAnchoredPosition(Vector2.Zero, size, anchor); + var (start, end) = GetArea(barPos + pos, size); + Rect background = new Rect(start, end - start, new(new(1, 1, 1, 0.5f))); + bar.SetBackground(background); + + return bar; + } + } + + [Exportable(false)] + public class UnitFramesRangeConfig : PluginConfigObject + { + [DragInt("Range (yalms)", min = 1, max = 500)] + [Order(5)] + public int Range = 30; + + [DragFloat("Alpha", min = 1, max = 100)] + [Order(10)] + public float Alpha = 24; + + [Checkbox("Use Additional Range Check")] + [Order(15)] + public bool UseAdditionalRangeCheck = false; + + [DragInt("Additional Range (yalms)", min = 1, max = 500)] + [Order(20, collapseWith = nameof(UseAdditionalRangeCheck))] + public int AdditionalRange = 15; + + [DragFloat("Additional Alpha", min = 1, max = 100)] + [Order(25, collapseWith = nameof(UseAdditionalRangeCheck))] + public float AdditionalAlpha = 60; + + public float AlphaForDistance(int distance, float alpha = 100f) + { + if (!Enabled) + { + return 100f; + } + + if (!UseAdditionalRangeCheck) + { + return distance > Range ? Alpha : alpha; + } + + if (Range > AdditionalRange) + { + return distance > Range ? Alpha : (distance > AdditionalRange ? AdditionalAlpha : alpha); + } + + return distance > AdditionalRange ? AdditionalAlpha : (distance > Range ? Alpha : alpha); + } + } +} diff --git a/Interface/GeneralElements/UnitFrameHud.cs b/Interface/GeneralElements/UnitFrameHud.cs new file mode 100644 index 0000000..b5e6fe5 --- /dev/null +++ b/Interface/GeneralElements/UnitFrameHud.cs @@ -0,0 +1,528 @@ +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Game.ClientState.Statuses; +using HSUI.Config; +using HSUI.Enums; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using FFXIVClientStructs.FFXIV.Client.System.Framework; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.FFXIV.Component.GUI; +using Dalamud.Bindings.ImGui; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using BattleChara = Dalamud.Game.ClientState.Objects.Types.IBattleChara; +using BattleNpcSubKind = Dalamud.Game.ClientState.Objects.Enums.BattleNpcSubKind; +using Character = Dalamud.Game.ClientState.Objects.Types.ICharacter; + +namespace HSUI.Interface.GeneralElements +{ + public unsafe class UnitFrameHud(UnitFrameConfig config, string displayName) + : DraggableHudElement(config, displayName), IHudElementWithActor, IHudElementWithMouseOver, IHudElementWithPreview, IHudElementWithVisibilityConfig + { + public UnitFrameConfig Config => (UnitFrameConfig)_config; + public VisibilityConfig VisibilityConfig => Config.VisibilityConfig; + + private SmoothHPHelper _smoothHPHelper = new SmoothHPHelper(); + + public IGameObject? Actor { get; set; } + + private bool _wasHovering = false; + + protected override (List, List) ChildrenPositionsAndSizes() + { + return (new List() { Config.Position }, new List() { Config.Size }); + } + + public void StopPreview() + { + Config.MouseoverAreaConfig.Preview = false; + Config.SignIconConfig.Preview = false; + } + + public void StopMouseover() + { + if (_wasHovering) + { + InputsHelper.Instance.ClearTarget(); + _wasHovering = false; + } + } + + public override void DrawChildren(Vector2 origin) + { + if (!Config.Enabled || Actor == null) + { + StopMouseover(); + return; + } + + DrawExtras(origin, Actor); + + if (Actor is Character character) + { + DrawCharacter(origin, character); + } + else + { + DrawFriendlyNPC(origin, Actor); + } + + // Check if mouse is hovering over the box properly + var startPos = Utils.GetAnchoredPosition(origin + Config.Position, Config.Size, Config.Anchor); + var (areaStart, areaEnd) = Config.MouseoverAreaConfig.GetArea(startPos, Config.Size); + bool isHovering = ImGui.IsMouseHoveringRect(areaStart, areaEnd); + bool ignoreMouseover = Config.MouseoverAreaConfig.Enabled && Config.MouseoverAreaConfig.Ignore; + + if (isHovering && !DraggingEnabled) + { + _wasHovering = true; + InputsHelper.Instance.SetTarget(Actor, ignoreMouseover); + + if (InputsHelper.Instance.LeftButtonClicked) + { + Plugin.TargetManager.Target = Actor; + } + else if (InputsHelper.Instance.RightButtonClicked) + { + AgentModule.Instance()->GetAgentHUD()->OpenContextMenuFromTarget((GameObject*)Actor.Address); + } + } + else if (_wasHovering) + { + InputsHelper.Instance.ClearTarget(); + _wasHovering = false; + } + } + + protected virtual void DrawExtras(Vector2 origin, IGameObject? actor) + { + // override + } + + private void DrawCharacter(Vector2 pos, Character character) + { + uint currentHp = character.CurrentHp; + uint maxHp = character.MaxHp; + + // fixes weird bug with npcs + if (maxHp == 1) + { + currentHp = 1; + } + else if (Config.SmoothHealthConfig.Enabled) + { + currentHp = _smoothHPHelper.GetNextHp((int)currentHp, (int)maxHp, Config.SmoothHealthConfig.Velocity); + } + + PluginConfigColor fillColor = ColorUtils.ColorForCharacter( + character, + currentHp, + maxHp, + Config.UseJobColor, + Config.UseRoleColor, + Config.ColorByHealth + ) ?? Config.FillColor; + + Rect background = new Rect(Config.Position, Config.Size, BackgroundColor(character)); + if (Config.RangeConfig.Enabled || Config.EnemyRangeConfig.Enabled) + { + fillColor = GetDistanceColor(character, fillColor); + background.Color = GetDistanceColor(character, background.Color); + } + + Rect healthFill = BarUtilities.GetFillRect(Config.Position, Config.Size, Config.FillDirection, fillColor, currentHp, maxHp); + + BarHud bar = new BarHud(Config, character); + bar.NeedsInputs = true; + bar.SetBackground(background); + bar.AddForegrounds(healthFill); + bar.AddLabels(GetLabels(maxHp)); + + if (Config.UseMissingHealthBar) + { + Vector2 healthMissingSize = Config.Size - BarUtilities.GetFillDirectionOffset(healthFill.Size, Config.FillDirection); + Vector2 healthMissingPos = Config.FillDirection.IsInverted() + ? Config.Position + : Config.Position + BarUtilities.GetFillDirectionOffset(healthFill.Size, Config.FillDirection); + + PluginConfigColor missingHealthColor = Config.UseJobColorAsMissingHealthColor && character is BattleChara + ? GlobalColors.Instance.SafeColorForJobId(character!.ClassJob.RowId) + : Config.UseRoleColorAsMissingHealthColor && character is BattleChara + ? GlobalColors.Instance.SafeRoleColorForJobId(character!.ClassJob.RowId) + : Config.HealthMissingColor; + + if (Config.UseDeathIndicatorBackgroundColor && character is BattleChara { CurrentHp: <= 0 }) + { + missingHealthColor = Config.DeathIndicatorBackgroundColor; + } + + if (Config.UseCustomInvulnerabilityColor && character is BattleChara battleChara) + { + IStatus? tankInvuln = Utils.GetTankInvulnerabilityID(battleChara); + if (tankInvuln is not null) + { + missingHealthColor = Config.CustomInvulnerabilityColor; + } + } + + if (Config.RangeConfig.Enabled || Config.EnemyRangeConfig.Enabled) + { + missingHealthColor = GetDistanceColor(character, missingHealthColor); + } + + bar.AddForegrounds(new Rect(healthMissingPos, healthMissingSize, missingHealthColor)); + } + + // shield + BarUtilities.AddShield(bar, Config, Config.ShieldConfig, character, healthFill.Size); + + // draw action + AddDrawActions(bar.GetDrawActions(pos, Config.StrataLevel)); + + // mouseover area + BarHud? mouseoverAreaBar = Config.MouseoverAreaConfig.GetBar( + Config.Position, + Config.Size, + Config.ID + "_mouseoverArea", + Config.Anchor + ); + + if (mouseoverAreaBar != null) + { + AddDrawActions(mouseoverAreaBar.GetDrawActions(pos, StrataLevel.HIGHEST)); + } + + // role/job icon + if (Config.RoleIconConfig.Enabled && character is IPlayerCharacter) + { + uint jobId = character.ClassJob.RowId; + uint iconId = Config.RoleIconConfig.UseRoleIcons ? + JobsHelper.RoleIconIDForJob(jobId, Config.RoleIconConfig.UseSpecificDPSRoleIcons) : + JobsHelper.IconIDForJob(jobId, (uint)Config.RoleIconConfig.Style); + + if (iconId > 0) + { + var barPos = Utils.GetAnchoredPosition(pos, Config.Size, Config.Anchor); + var parentPos = Utils.GetAnchoredPosition(barPos + Config.Position, -Config.Size, Config.RoleIconConfig.FrameAnchor); + var iconPos = Utils.GetAnchoredPosition(parentPos + Config.RoleIconConfig.Position, Config.RoleIconConfig.Size, Config.RoleIconConfig.Anchor); + + AddDrawAction(Config.RoleIconConfig.StrataLevel, () => + { + DrawHelper.DrawInWindow(ID + "_jobIcon", iconPos, Config.RoleIconConfig.Size, false, (drawList) => + { + DrawHelper.DrawIcon(iconId, iconPos, Config.RoleIconConfig.Size, false, drawList); + }); + }); + } + } + + // sign icon + if (Config.SignIconConfig.Enabled) + { + uint? iconId = Config.SignIconConfig.IconID(character); + if (iconId.HasValue) + { + var barPos = Utils.GetAnchoredPosition(pos, Config.Size, Config.Anchor); + var parentPos = Utils.GetAnchoredPosition(barPos + Config.Position, -Config.Size, Config.SignIconConfig.FrameAnchor); + var iconPos = Utils.GetAnchoredPosition(parentPos + Config.SignIconConfig.Position, Config.SignIconConfig.Size, Config.SignIconConfig.Anchor); + + AddDrawAction(Config.SignIconConfig.StrataLevel, () => + { + DrawHelper.DrawInWindow(ID + "_signIcon", iconPos, Config.SignIconConfig.Size, false, (drawList) => + { + DrawHelper.DrawIcon(iconId.Value, iconPos, Config.SignIconConfig.Size, false, drawList); + }); + }); + } + } + } + + private LabelConfig[] GetLabels(uint maxHp) + { + List labels = new List(); + + if (Config.HideHealthIfPossible && maxHp <= 1) + { + if (!Utils.IsHealthLabel(Config.LeftLabelConfig)) + { + labels.Add(Config.LeftLabelConfig); + } + + if (!Utils.IsHealthLabel(Config.RightLabelConfig)) + { + labels.Add(Config.RightLabelConfig); + } + + if (!Utils.IsHealthLabel(Config.OptionalLabelConfig)) + { + labels.Add(Config.OptionalLabelConfig); + } + } + else + { + labels.Add(Config.LeftLabelConfig); + labels.Add(Config.RightLabelConfig); + labels.Add(Config.OptionalLabelConfig); + } + + return labels.ToArray(); + } + + + + private PluginConfigColor GetDistanceColor(Character? character, PluginConfigColor color) + { + byte distance = character != null ? character.YalmDistanceX : byte.MaxValue; + float currentAlpha = color.Vector.W * 100f; + float alpha = Config.RangeConfig.AlphaForDistance(distance, currentAlpha) / 100f; + + if (character is IBattleNpc { BattleNpcKind: BattleNpcSubKind.Enemy or BattleNpcSubKind.BattleNpcPart } && Config.EnemyRangeConfig.Enabled) + { + alpha = Config.EnemyRangeConfig.AlphaForDistance(distance, currentAlpha) / 100f; + } + + return color.WithAlpha(alpha); + } + + private unsafe void GetNPCHpValues(IGameObject? actor, out uint currentHp, out uint maxHp) + { + currentHp = 0; + maxHp = 0; + + var player = Plugin.ObjectTable.LocalPlayer; + if (player == null || actor == null || player.TargetObject == null || actor.GameObjectId != player.TargetObject.GameObjectId) + { + return; + } + + AtkUnitBase* TargetWidget = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("_TargetInfoMainTarget", 1).Address; + if (TargetWidget != null) + { + AtkTextNode* textNode = TargetWidget->GetTextNodeById(11); + string integrityText = textNode->NodeText.ToString(); + + // not a gathering node or node at 100%, nothing to do + if (!integrityText.Contains("%")) + { + return; + } + + try + { + currentHp = Convert.ToUInt32((integrityText.Replace("%", ""))); + maxHp = 100; + } + catch { } + } + } + + private void DrawFriendlyNPC(Vector2 pos, IGameObject? actor) + { + GetNPCHpValues(actor, out uint currentHp, out uint maxHp); + + BarHud bar = new BarHud(Config, actor); + bar.AddLabels(GetLabels(0)); + + if (maxHp == 0) + { + bar.AddForegrounds(new Rect(Config.Position, Config.Size, ColorUtils.ColorForActor(actor))); + } + else + { + if (Config.SmoothHealthConfig.Enabled) + { + currentHp = _smoothHPHelper.GetNextHp((int)currentHp, (int)maxHp, Config.SmoothHealthConfig.Velocity); + } + + PluginConfigColor fillColor = ColorUtils.ColorForCharacter( + actor, + currentHp, + maxHp, + colorByHealthConfig: Config.ColorByHealth + ) ?? Config.FillColor; + + Rect background = new Rect(Config.Position, Config.Size, Config.BackgroundColor); + Rect healthFill = BarUtilities.GetFillRect(Config.Position, Config.Size, Config.FillDirection, fillColor, currentHp, maxHp); + + bar.NeedsInputs = true; + bar.SetBackground(background); + bar.AddForegrounds(healthFill); + } + + AddDrawActions(bar.GetDrawActions(pos, Config.StrataLevel)); + } + + private PluginConfigColor BackgroundColor(Character? chara) + { + if (Config.ShowTankInvulnerability && + !Config.UseMissingHealthBar && + chara is BattleChara battleChara) + { + IStatus? tankInvuln = Utils.GetTankInvulnerabilityID(battleChara); + + if (tankInvuln != null) + { + PluginConfigColor color; + if (Config.UseCustomInvulnerabilityColor) + { + color = Config.CustomInvulnerabilityColor; + } + else if (tankInvuln.StatusId == 811 && Config.UseCustomWalkingDeadColor) + { + color = Config.CustomWalkingDeadColor; + } + else + { + color = new PluginConfigColor(GlobalColors.Instance.SafeColorForJobId(chara.ClassJob.RowId).Vector.AdjustColor(-.8f)); + } + + return color; + } + } + + if (chara is BattleChara) + { + if (Config.UseJobColorAsBackgroundColor) + { + return GlobalColors.Instance.SafeColorForJobId(chara.ClassJob.RowId); + } + else if (Config.UseRoleColorAsBackgroundColor) + { + return GlobalColors.Instance.SafeRoleColorForJobId(chara.ClassJob.RowId); + } + else if (Config.UseDeathIndicatorBackgroundColor && chara.CurrentHp <= 0) + { + return Config.DeathIndicatorBackgroundColor; + } + else + { + return Config.BackgroundColor; + } + } + + return GlobalColors.Instance.EmptyUnitFrameColor; + } + } + + public class PlayerUnitFrameHud : UnitFrameHud + { + public new PlayerUnitFrameConfig Config => (PlayerUnitFrameConfig)_config; + + public PlayerUnitFrameHud(PlayerUnitFrameConfig config, string displayName) : base(config, displayName) + { + + } + + protected override void DrawExtras(Vector2 origin, IGameObject? actor) + { + TankStanceIndicatorConfig config = Config.TankStanceIndicatorConfig; + + if (!config.Enabled || actor is not IPlayerCharacter chara) { return; } + + uint jobId = chara.ClassJob.RowId; + if (JobsHelper.RoleForJob(jobId) != JobRoles.Tank) { return; } + + var tankStanceBuff = Utils.StatusListForBattleChara(chara).Where(o => + o.StatusId == 79 || // IRON WILL + o.StatusId == 91 || // DEFIANCE + o.StatusId == 392 || // ROYAL GUARD + o.StatusId == 393 || // IRON WILL + o.StatusId == 743 || // GRIT + o.StatusId == 1396 || // DEFIANCE + o.StatusId == 1397 || // GRIT + o.StatusId == 1833 // ROYAL GUARD + ); + + PluginConfigColor color = tankStanceBuff.Any() ? config.ActiveColor : config.InactiveColor; + + Vector2 pos = GetTankStanceCornerOrigin(origin); + var (verticalDir, horizontalDir) = GetTankStanceLinesDirections(); + + pos = new Vector2(pos.X + config.Thickess * -horizontalDir, pos.Y + config.Thickess * -verticalDir); + Vector2 vSize = new Vector2(config.Thickess * horizontalDir, (config.Size.Y + config.Thickess) * verticalDir); + Vector2 vEndPos = pos + vSize; + Vector2 hSize = new Vector2((config.Size.X + config.Thickess) * horizontalDir, config.Thickess * verticalDir); + Vector2 hEndPos = pos + hSize; + + Vector2 startPos = new Vector2(Math.Min(pos.X, hEndPos.X), Math.Min(pos.Y, hEndPos.Y)); + Vector2 endPos = new Vector2(Math.Max(pos.X, hEndPos.X), Math.Max(pos.Y, hEndPos.Y)); ; + + AddDrawAction(StrataLevel.LOWEST, () => + { + DrawHelper.DrawInWindow(ID + "_TankStance", startPos, endPos - startPos, false, (drawList) => + { + // TODO: clean up hacky math + // there's some 1px errors prob due to negative sizes + // couldn't figure it out so I did the hacky fixes + + // vertical + + drawList.AddRectFilled(pos, vEndPos, color.Base); + + if (config.Corner == TankStanceCorner.TopRight) + { + drawList.AddLine(pos, pos + new Vector2(0, vSize.Y + 1), 0xFF000000); + } + else + { + drawList.AddLine(pos, pos + new Vector2(0, vSize.Y), 0xFF000000); + } + + drawList.AddLine(pos + vSize, pos + vSize + new Vector2(-vSize.X, 0), 0xFF000000); + + // horizontal + drawList.AddRectFilled(pos, hEndPos, color.Base); + + if (config.Corner == TankStanceCorner.BottomLeft) + { + drawList.AddLine(pos, pos + new Vector2(hSize.X + 1, 0), 0xFF000000); + } + else + { + drawList.AddLine(pos, pos + new Vector2(hSize.X, 0), 0xFF000000); + } + + if (config.Corner == TankStanceCorner.BottomRight) + { + drawList.AddLine(pos + new Vector2(0, 1), pos + new Vector2(0, hSize.Y), 0xFF000000); + } + else + { + drawList.AddLine(pos, pos + new Vector2(0, hSize.Y), 0xFF000000); + } + + drawList.AddLine(pos + hSize, pos + hSize + new Vector2(0, -hSize.Y), 0xFF000000); + }); + }); + } + + private Vector2 GetTankStanceCornerOrigin(Vector2 origin) + { + var topLeft = Utils.GetAnchoredPosition(origin + Config.Position, Config.Size, Config.Anchor); + + return Config.TankStanceIndicatorConfig.Corner switch + { + TankStanceCorner.TopRight => topLeft + new Vector2(Config.Size.X - 1, 0), + TankStanceCorner.BottomLeft => topLeft + new Vector2(0, Config.Size.Y - 1), + TankStanceCorner.BottomRight => topLeft + Config.Size - Vector2.One, + _ => topLeft + }; + } + + private (int, int) GetTankStanceLinesDirections() + { + return Config.TankStanceIndicatorConfig.Corner switch + { + TankStanceCorner.TopLeft => (1, 1), + TankStanceCorner.TopRight => (1, -1), + TankStanceCorner.BottomLeft => (-1, 1), + _ => (-1, -1) + }; + } + } +} \ No newline at end of file diff --git a/Interface/GeneralElements/VisibilityConfig.cs b/Interface/GeneralElements/VisibilityConfig.cs new file mode 100644 index 0000000..a0d2c2f --- /dev/null +++ b/Interface/GeneralElements/VisibilityConfig.cs @@ -0,0 +1,168 @@ +using Dalamud.Game.ClientState.Conditions; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.ClientState.Objects.SubKinds; +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Interface.GeneralElements; +using HSUI.Interface.Party; +using System.Linq; + +namespace HSUI.Interface +{ + [Exportable(false)] + public class VisibilityConfig : PluginConfigObject + { + [Checkbox("Hide outside of combat")] + [Order(5)] + public bool HideOutsideOfCombat = false; + + [Checkbox("Hide in combat")] + [Order(6)] + public bool HideInCombat = false; + + [Checkbox("Hide in Gold Saucer")] + [Order(7)] + public bool HideInGoldSaucer = false; + + [Checkbox("Hide while at full HP")] + [Order(8)] + public bool HideOnFullHP = false; + + [Checkbox("Hide when in duty")] + [Order(9)] + public bool HideInDuty = false; + + [Checkbox("Hide in Island Sanctuary")] + [Order(10)] + public bool HideInIslandSanctuary = false; + + [Checkbox("Hide in PvP")] + [Order(11)] + public bool HideInPvP = false; + + [Checkbox("Always show when in duty")] + [Order(20)] + public bool ShowInDuty = false; + + [Checkbox("Always show when weapon is drawn")] + [Order(21)] + public bool ShowOnWeaponDrawn = false; + + [Checkbox("Always show when crafting")] + [Order(22)] + public bool ShowWhileCrafting = false; + + [Checkbox("Always show when gathering")] + [Order(23)] + public bool ShowWhileGathering = false; + + [Checkbox("Always show while in a party")] + [Order(24)] + public bool ShowInParty = false; + + [Checkbox("Always show while in Island Sanctuary")] + [Order(25)] + public bool ShowInIslandSanctuary = false; + + [Checkbox("Always show while in PvP")] + [Order(26)] + public bool ShowInPvP = false; + + [Checkbox("Always show while target exists")] + [Order(27)] + public bool ShowWhileTargetExists = false; + + + private bool IsInCombat() => Plugin.Condition[ConditionFlag.InCombat]; + + private bool IsInDuty() => Plugin.Condition[ConditionFlag.BoundByDuty]; + + private bool IsCrafting() => Plugin.Condition[ConditionFlag.Crafting] || Plugin.Condition[ConditionFlag.ExecutingCraftingAction]; + + private bool IsGathering() => Plugin.Condition[ConditionFlag.Gathering] || Plugin.Condition[ConditionFlag.ExecutingGatheringAction]; + + private bool HasWeaponDrawn() => (Plugin.ObjectTable.LocalPlayer != null && Plugin.ObjectTable.LocalPlayer.StatusFlags.HasFlag(StatusFlags.WeaponOut)); + + private bool IsInGoldSaucer() => _goldSaucerIDs.Any(id => id == Plugin.ClientState.TerritoryType); + + private bool IsInIslandSanctuary() => Plugin.ClientState.TerritoryType == 1055; + + private readonly uint[] _goldSaucerIDs = { 144, 388, 389, 390, 391, 579, 792, 899, 941 }; + + public bool IsElementVisible(HudElement? element = null) + { + if (!Enabled) { return true; } + if (!ConfigurationManager.Instance.LockHUD) { return true; } + if (element != null && element.GetType() == typeof(PlayerCastbarHud)) { return true; } + if (element != null && !element.GetConfig().Enabled) { return false; } + + bool isInIslandSanctuary = IsInIslandSanctuary(); + bool isInDuty = IsInDuty() && !isInIslandSanctuary; + IPlayerCharacter? player = Plugin.ObjectTable.LocalPlayer; + + // show + if (ShowInDuty && isInDuty) { return true; } + + if (ShowOnWeaponDrawn && HasWeaponDrawn()) { return true; } + + if (ShowWhileCrafting && IsCrafting()) { return true; } + + if (ShowWhileGathering && IsGathering()) { return true; } + + if (ShowInParty && PartyManager.Instance.MemberCount > 1) { return true; } + + if (ShowInIslandSanctuary && isInIslandSanctuary) { return true; } + + if (ShowInPvP && Plugin.ClientState.IsPvP) { return true; } + + if (ShowWhileTargetExists && player != null && player.TargetObject != null) { return true; } + + + // hide + if (HideOutsideOfCombat && !IsInCombat()) { return false; } + + if (HideInCombat && IsInCombat()) { return false; } + + if (HideInGoldSaucer && IsInGoldSaucer()) { return false; } + + if (HideOnFullHP && player != null && player.CurrentHp == player.MaxHp) { return false; } + + if (HideInDuty && isInDuty) { return false; } + + if (HideInIslandSanctuary && isInIslandSanctuary) { return false; } + + if (HideInPvP && Plugin.ClientState.IsPvP) { return false; } + + return true; + } + + public void CopyFrom(VisibilityConfig config) + { + Enabled = config.Enabled; + + HideOutsideOfCombat = config.HideOutsideOfCombat; + HideInCombat = config.HideInCombat; + HideInGoldSaucer = config.HideInGoldSaucer; + HideOnFullHP = config.HideOnFullHP; + HideInDuty = config.HideInDuty; + HideInIslandSanctuary = config.HideInIslandSanctuary; + HideInPvP = config.HideInPvP; + + ShowInDuty = config.ShowInDuty; + ShowOnWeaponDrawn = config.ShowOnWeaponDrawn; + ShowWhileCrafting = config.ShowWhileCrafting; + ShowWhileGathering = config.ShowWhileGathering; + ShowInParty = config.ShowInParty; + ShowInIslandSanctuary = config.ShowInIslandSanctuary; + ShowInPvP = config.ShowInPvP; + ShowWhileTargetExists = config.ShowWhileTargetExists; + } + + public VisibilityConfig() + { + Enabled = false; + } + } +} + + diff --git a/Interface/GeneralElements/WindowClippingConfig.cs b/Interface/GeneralElements/WindowClippingConfig.cs new file mode 100644 index 0000000..82a8139 --- /dev/null +++ b/Interface/GeneralElements/WindowClippingConfig.cs @@ -0,0 +1,151 @@ +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Helpers; +using Dalamud.Bindings.ImGui; + +namespace HSUI.Interface.GeneralElements +{ + public enum WindowClippingMode + { + Full, + Hide, + Performance + } + + [Exportable(false)] + [Disableable(false)] + [Section("Misc")] + [SubSection("Window Clipping", 0)] + public class WindowClippingConfig : PluginConfigObject + { + public new static WindowClippingConfig DefaultConfig() => new WindowClippingConfig(); + + public WindowClippingMode Mode = WindowClippingMode.Full; + + public bool NameplatesClipRectsEnabled = true; + public bool TargetCastbarClipRectEnabled = false; + public bool HotbarsClipRectsEnabled = false; + public bool ChatBubblesPlayersClipRectsEnabled = true; + public bool ChatBubblesNPCClipRectsEnabled = true; + + public bool ThirdPartyClipRectsEnabled = true; + + private bool _showConfirmationDialog = false; + + [ManualDraw] + public bool Draw(ref bool changed) + { + ImGuiHelper.NewLineAndTab(); + + if (ImGui.Checkbox("Enabled", ref Enabled)) + { + if (Enabled) + { + Enabled = false; + _showConfirmationDialog = true; + } + else + { + changed = true; + } + } + + // confirmation dialog + if (_showConfirmationDialog) + { + string[] lines = new string[] { "THIS FEATURE IS KNOWN TO CAUSE RANDOM", "CRASHES TO A SMALL PORTION OF USERS!!!", "Are you sure you want to enable it?" }; + var (didConfirm, didClose) = ImGuiHelper.DrawConfirmationModal("WARNING!", lines); + + if (didConfirm) + { + Enabled = true; + changed = true; + } + + if (didConfirm || didClose) + { + _showConfirmationDialog = false; + } + } + + if (!Enabled) { return changed; } + + // mode + ImGuiHelper.NewLineAndTab(); + ImGui.SameLine(); + ImGui.Text("Mode: "); + + ImGui.SameLine(); + if (ImGui.RadioButton("Full", Mode == WindowClippingMode.Full)) + { + Mode = WindowClippingMode.Full; + } + + ImGui.SameLine(); + if (ImGui.RadioButton("Hide", Mode == WindowClippingMode.Hide)) + { + Mode = WindowClippingMode.Hide; + } + + ImGui.SameLine(); + if (ImGui.RadioButton("Performance", Mode == WindowClippingMode.Performance)) + { + Mode = WindowClippingMode.Performance; + } + + // nameplates + ImGui.NewLine(); + ImGuiHelper.NewLineAndTab(); + changed |= ImGui.Checkbox("Enable special clipping for Nameplates", ref NameplatesClipRectsEnabled); + ImGuiHelper.SetTooltip("When enabled, Nameplates will get covered by game UI elements that wouldn't normally cover HSUI elements."); + + if (NameplatesClipRectsEnabled) + { + ImGuiHelper.Tab(); ImGuiHelper.Tab(); + changed |= ImGui.Checkbox("Default Target Castbar", ref TargetCastbarClipRectEnabled); + ImGuiHelper.SetTooltip("When enabled, the game's target castbar will not be covered by HSUI Nameplates.\nFor players that prefer to use the default target cast bar over HSUI's."); + + ImGuiHelper.Tab(); ImGuiHelper.Tab(); + changed |= ImGui.Checkbox("Hotbars", ref HotbarsClipRectsEnabled); + ImGuiHelper.SetTooltip("When enabled, active hotbar will not be covered by HSUI Nameplates.\nNote that the way this is calculated is not perfect and it might not work well for hotbars that have empty slots."); + + ImGuiHelper.Tab(); ImGuiHelper.Tab(); + changed |= ImGui.Checkbox("NPC Chat Bubbles", ref ChatBubblesNPCClipRectsEnabled); + + ImGuiHelper.Tab(); ImGuiHelper.Tab(); + changed |= ImGui.Checkbox("Player Chat Bubbles", ref ChatBubblesPlayersClipRectsEnabled); + } + + // third party + ImGui.NewLine(); + ImGuiHelper.NewLineAndTab(); + changed |= ImGui.Checkbox("Enable clipping for other plugins", ref ThirdPartyClipRectsEnabled); + ImGuiHelper.SetTooltip("When enabled, other plugins' windows can also be clipped so HSUI elements don't cover them.\nPlease note that this requires the developer of each third party plugin to implement the feature."); + + // text + ImGui.NewLine(); + ImGuiHelper.NewLineAndTab(); + ImGui.SameLine(); + + switch (Mode) + { + case WindowClippingMode.Full: + ImGui.Text("HSUI will attempt to not cover game windows in this mode by clipping around them."); + break; + + case WindowClippingMode.Hide: + ImGui.Text("HSUI will attempt to not cover game windows in this mode by not drawing an element if its touching a game window."); + break; + + case WindowClippingMode.Performance: + ImGui.Text("Window Clipping functionality will be reduced in favor of performance.\nOnly one game window will be clipped at a time. This might yield unexpected / ugly results.\n\nNote: This mode won't work well with Nameplates."); + break; + } + + ImGuiHelper.NewLineAndTab(); + ImGui.Text("If you're experiencing random crashes or bad performance, we recommend you try a different mode\nor disable Window Clipping altogether"); + + return false; + } + } +} diff --git a/Interface/HudElement.cs b/Interface/HudElement.cs new file mode 100644 index 0000000..53e48af --- /dev/null +++ b/Interface/HudElement.cs @@ -0,0 +1,120 @@ +using HSUI.Config; +using System.Numerics; +using Dalamud.Game.ClientState.Objects.Types; +using System; +using System.Collections.Generic; +using HSUI.Enums; + +namespace HSUI.Interface +{ + public abstract class HudElement : IDisposable + { + protected MovablePluginConfigObject _config; + public MovablePluginConfigObject GetConfig() { return _config; } + + public string ID => _config.ID; + + private Dictionary> _drawActions = new Dictionary>(); + + public HudElement(MovablePluginConfigObject config) + { + _config = config; + } + + public void PrepareForDraw(Vector2 origin) + { + _drawActions.Clear(); + CreateDrawActions(origin); + } + + public virtual void Draw(Vector2 origin) + { + // iterate like this so it goes in order + StrataLevel[] levels = (StrataLevel[])Enum.GetValues(typeof(StrataLevel)); + foreach (StrataLevel key in levels) + { + _drawActions.TryGetValue(key, out List? drawActions); + if (drawActions == null) { continue; } + + foreach (Action drawAction in _drawActions[key]) + { + drawAction(); + } + } + } + + protected void AddDrawAction(StrataLevel strataLevel, Action drawAction) + { + _drawActions.TryGetValue(strataLevel, out List? drawActions); + + if (drawActions == null) + { + drawActions = new List(); + _drawActions.Add(strataLevel, drawActions); + } + + drawActions.Add(drawAction); + } + + protected void AddDrawActions(List<(StrataLevel, Action)> drawActions) + { + foreach ((StrataLevel strataLevel, Action drawAction) in drawActions) + { + AddDrawAction(strataLevel, drawAction); + } + } + + protected abstract void CreateDrawActions(Vector2 origin); + + ~HudElement() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + + InternalDispose(); + } + + protected virtual void InternalDispose() + { + // override + } + } + + public interface IHudElementWithActor + { + public IGameObject? Actor { get; set; } + } + + public interface IHudElementWithAnchorableParent + { + public AnchorablePluginConfigObject? ParentConfig { get; set; } + } + + public interface IHudElementWithMouseOver + { + public void StopMouseover(); + } + + public interface IHudElementWithPreview + { + public void StopPreview(); + } + + public interface IHudElementWithVisibilityConfig + { + public VisibilityConfig? VisibilityConfig { get; } + } +} \ No newline at end of file diff --git a/Interface/HudHelper.cs b/Interface/HudHelper.cs new file mode 100644 index 0000000..16b69fe --- /dev/null +++ b/Interface/HudHelper.cs @@ -0,0 +1,506 @@ +using Dalamud.Game.Addon.Lifecycle; +using Dalamud.Game.ClientState.Conditions; +using Dalamud.Game.ClientState.Objects.SubKinds; +using HSUI.Config; +using HSUI.Helpers; +using HSUI.Interface.EnemyList; +using HSUI.Interface.GeneralElements; +using HSUI.Interface.Party; +using HSUI.Interface.Jobs; +using HSUI.Interface.Nameplates; +using HSUI.Interface.StatusEffects; +using FFXIVClientStructs.FFXIV.Component.GUI; +using System; +using System.Collections.Generic; +using System.Numerics; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Client.UI.Misc; + +namespace HSUI.Interface +{ + public class HudHelper : IDisposable + { + private HUDOptionsConfig Config => ConfigurationManager.Instance.GetConfigObject(); + + private bool _firstUpdate = true; + + private Vector2 _castBarPos = Vector2.Zero; + private bool _hidingCastBar = false; + private Vector2 _pullTimerPos = Vector2.Zero; + private bool _hidingPullTimer = false; + + public HudHelper() + { + Config.ValueChangeEvent += ConfigValueChanged; + } + + ~HudHelper() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + + Config.ValueChangeEvent -= ConfigValueChanged; + + // Only restore defaults when already on framework thread. Skip RunOnFrameworkThread + // during unload—it can deadlock. Restore is best-effort; game state may be torn down. + if (Plugin.Framework.IsInFrameworkUpdateThread && Plugin.ObjectTable.LocalPlayer != null) + { + try + { + SetGameHudElementsHidden(Array.Empty(), true); + UpdateDefaultCastBar(true); + UpdateDefaultPulltimer(true); + UpdateDefaultNameplates(true); + UpdateJobGauges(true); + } + catch (Exception ex) + { + Plugin.Logger.Error($"Exception during HudHelper.Dispose restore: {ex.Message}"); + } + } + } + + public void Update() + { + try + { + if (_firstUpdate) + { + _firstUpdate = false; + UpdateDefaultCastBar(); + UpdateDefaultPulltimer(); + } + + UpdateJobGauges(); + UpdateDefaultHudElementsHidden(); + UpdateDefaultNameplates(); + } + catch (Exception ex) + { + var now = DateTime.UtcNow; + if ((now - _lastHudElementsErrorLog).TotalSeconds >= HudElementsErrorLogIntervalSeconds) + { + _lastHudElementsErrorLog = now; + Plugin.Logger.Warning($"[HSUI] HudHelper.Update: {ex.Message}"); + } + } + } + + public bool IsElementHidden(HudElement element) + { + IHudElementWithVisibilityConfig? e = element as IHudElementWithVisibilityConfig; + if (e == null || e.VisibilityConfig == null) { return false; } + + return !e.VisibilityConfig.IsElementVisible(element); + } + + private void ConfigValueChanged(object sender, OnChangeBaseArgs e) + { + switch (e.PropertyName) + { + case "HideDefaultHudWhenReplaced": + UpdateDefaultHudElementsHidden(); + break; + case "HideDefaultCastbar": + UpdateDefaultCastBar(); + break; + case "HideDefaultPulltimer": + UpdateDefaultPulltimer(); + break; + } + } + + private static DateTime _lastHudElementsErrorLog = DateTime.MinValue; + private const double HudElementsErrorLogIntervalSeconds = 10.0; + + private void UpdateDefaultHudElementsHidden() + { + try + { + UpdateDefaultHudElementsHiddenCore(); + } + catch (Exception ex) + { + var now = DateTime.UtcNow; + if ((now - _lastHudElementsErrorLog).TotalSeconds >= HudElementsErrorLogIntervalSeconds) + { + _lastHudElementsErrorLog = now; + Plugin.Logger.Warning($"[HSUI] HudHelper: skipped HUD hide update (resolver not ready): {ex.Message}"); + } + } + } + + private void UpdateDefaultHudElementsHiddenCore() + { + if (Plugin.Condition.Any( + ConditionFlag.OccupiedInEvent, + ConditionFlag.OccupiedInQuestEvent, + ConditionFlag.OccupiedInCutSceneEvent, + ConditionFlag.OccupiedSummoningBell, + ConditionFlag.Occupied, + ConditionFlag.Occupied30, + ConditionFlag.Occupied33, + ConditionFlag.Occupied38, + ConditionFlag.Occupied39, + ConditionFlag.WatchingCutscene, + ConditionFlag.WatchingCutscene78, + ConditionFlag.CreatingCharacter, + ConditionFlag.BetweenAreas, + ConditionFlag.BetweenAreas51, + ConditionFlag.BoundByDuty95, + ConditionFlag.ChocoboRacing, + ConditionFlag.PlayingLordOfVerminion) + || Plugin.ClientState.IsPvP) + { + return; + } + + if (!Config.HideDefaultHudWhenReplaced) + { + SetGameHudElementsHidden(Array.Empty(), false); + return; + } + + var hashesToHide = new List(); + void AddHashes(params string[] addonNames) + { + foreach (var name in addonNames) + { + var h = HudLayoutHashHelper.GetHash(name); + if (h != 0) + hashesToHide.Add(h); + } + } + + var hotbarsConfig = ConfigurationManager.Instance?.GetConfigObject(); + if (hotbarsConfig?.Enabled == true) + { + AddHashes("_ActionBar", "_ActionBar01", "_ActionBar02", "_ActionBar03", "_ActionBar04", + "_ActionBar05", "_ActionBar06", "_ActionBar07", "_ActionBar08", "_ActionBar09", "_ActionCross"); + } + + var playerUnitFrame = ConfigurationManager.Instance?.GetConfigObject(); + if (playerUnitFrame?.Enabled == true) + AddHashes("_ParameterWidget"); + + var targetUnitFrame = ConfigurationManager.Instance?.GetConfigObject(); + if (targetUnitFrame?.Enabled == true) + AddHashes("_TargetInfo", "_TargetInfoMainTarget", "_TargetInfoCastBar", "_TargetInfoBuffDebuff"); + + var focusUnitFrame = ConfigurationManager.Instance?.GetConfigObject(); + if (focusUnitFrame?.Enabled == true) + AddHashes("_FocusTargetInfo"); + + var playerCastbar = ConfigurationManager.Instance?.GetConfigObject(); + if (playerCastbar?.Enabled == true) + AddHashes("_CastBar"); + + var expBar = ConfigurationManager.Instance?.GetConfigObject(); + if (expBar?.Enabled == true) + AddHashes("_Exp"); + + var limitBreak = ConfigurationManager.Instance?.GetConfigObject(); + if (limitBreak?.Enabled == true) + AddHashes("_LimitBreak"); + + var playerBuffs = ConfigurationManager.Instance?.GetConfigObject(); + var playerDebuffs = ConfigurationManager.Instance?.GetConfigObject(); + if (playerBuffs?.Enabled == true || playerDebuffs?.Enabled == true) + AddHashes("_Status", "_StatusCustom0", "_StatusCustom1", "_StatusCustom2", "_StatusCustom3"); + + var partyFrames = ConfigurationManager.Instance?.GetConfigObject(); + if (partyFrames?.Enabled == true) + AddHashes("_PartyList"); + + var enemyList = ConfigurationManager.Instance?.GetConfigObject(); + if (enemyList?.Enabled == true) + AddHashes("_EnemyList"); + + var nameplatesConfig = ConfigurationManager.Instance?.GetConfigObject(); + if (nameplatesConfig?.Enabled == true) + AddHashes("NamePlate"); + + if (IsCurrentJobHudEnabled()) + { + foreach (var name in GetCurrentJobGaugeAddonNames()) + AddHashes(name); + } + + SetGameHudElementsHidden(hashesToHide.ToArray(), false); + } + + /// Visibility (ByteValue2) of game HUD elements before we hid them. Restored on disable/unload. + private readonly Dictionary _gameHudVisibilityBeforeHide = new(); + + private unsafe void SetGameHudElementsHidden(uint[] hashesToHide, bool forceRestore) + { + AddonConfig* config = AddonConfig.Instance(); + if (config == null || !config->IsLoaded || config->ActiveDataSet == null) + return; + + var hashSet = new HashSet(hashesToHide); + Span entries = config->ActiveDataSet->HudLayoutConfigEntries; + bool hasChanges = false; + + if (forceRestore || hashesToHide.Length == 0) + { + foreach (ref var entry in entries) + { + if (!_gameHudVisibilityBeforeHide.TryGetValue(entry.AddonNameHash, out byte saved)) + continue; + if (entry.ByteValue2 == saved) + continue; + entry.ByteValue2 = saved; + hasChanges = true; + } + _gameHudVisibilityBeforeHide.Clear(); + } + else + { + foreach (ref var entry in entries) + { + if (!hashSet.Contains(entry.AddonNameHash)) + continue; + if (!_gameHudVisibilityBeforeHide.ContainsKey(entry.AddonNameHash)) + _gameHudVisibilityBeforeHide[entry.AddonNameHash] = entry.ByteValue2; + if (entry.ByteValue2 == 0x0) + continue; + entry.ByteValue2 = 0x0; + hasChanges = true; + } + } + + if (hasChanges) + { + config->SaveFile(true); + config->ApplyHudLayout(); + } + } + + private unsafe void UpdateDefaultCastBar(bool forceVisible = false) + { + if (Config.HideDefaultCastbar && !_hidingCastBar) + { + Plugin.AddonLifecycle.RegisterListener(AddonEvent.PreDraw, "_CastBar", (addonEvent, args) => + { + AtkUnitBase* addon = (AtkUnitBase*)args.Addon.Address; + + if (!_hidingCastBar) + { + _castBarPos = new Vector2(addon->RootNode->GetXFloat(), addon->RootNode->GetYFloat()); + } + + addon->RootNode->SetPositionFloat(-9999.0f, -9999.0f); + }); + + _hidingCastBar = true; + } + else if ((forceVisible || !Config.HideDefaultCastbar) && _hidingCastBar) + { + Plugin.AddonLifecycle.UnregisterListener(AddonEvent.PreDraw, "_CastBar"); + + AtkUnitBase* addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("_CastBar", 1).Address; + if (addon != null) + { + addon->RootNode->SetPositionFloat(_castBarPos.X, _castBarPos.Y); + } + + _hidingCastBar = false; + } + + return; + } + + private unsafe void UpdateDefaultPulltimer(bool forceVisible = false) + { + if (Config.HideDefaultPulltimer && !_hidingPullTimer) + { + Plugin.AddonLifecycle.RegisterListener(AddonEvent.PreDraw, "ScreenInfo_CountDown", (addonEvent, args) => + { + AtkUnitBase* addon = (AtkUnitBase*)args.Addon.Address; + + if (!_hidingPullTimer) + { + _pullTimerPos = new Vector2(addon->RootNode->GetXFloat(), addon->RootNode->GetYFloat()); + } + + addon->RootNode->SetPositionFloat(-9999.0f, -9999.0f); + }); + + AtkUnitBase* addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("ScreenInfo_CountDown", 1).Address; + if (addon != null) + { + _pullTimerPos = new Vector2(addon->RootNode->GetXFloat(), addon->RootNode->GetYFloat()); + addon->RootNode->SetPositionFloat(-9999, -9999); + } + + _hidingPullTimer = true; + } + else if ((forceVisible || !Config.HideDefaultPulltimer) && _hidingPullTimer) + { + Plugin.AddonLifecycle.UnregisterListener(AddonEvent.PreDraw, "ScreenInfo_CountDown"); + + AtkUnitBase* addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("ScreenInfo_CountDown", 1).Address; + if (addon != null) + { + addon->RootNode->SetPositionFloat(_pullTimerPos.X, _pullTimerPos.Y); + } + + _hidingPullTimer = false; + } + + return; + } + + private static readonly Dictionary _jobIdToConfigType = new() + { + [JobIDs.PLD] = typeof(PaladinConfig), [JobIDs.WAR] = typeof(WarriorConfig), + [JobIDs.DRK] = typeof(DarkKnightConfig), [JobIDs.GNB] = typeof(GunbreakerConfig), + [JobIDs.WHM] = typeof(WhiteMageConfig), [JobIDs.SCH] = typeof(ScholarConfig), + [JobIDs.AST] = typeof(AstrologianConfig), [JobIDs.SGE] = typeof(SageConfig), + [JobIDs.MNK] = typeof(MonkConfig), [JobIDs.DRG] = typeof(DragoonConfig), + [JobIDs.NIN] = typeof(NinjaConfig), [JobIDs.SAM] = typeof(SamuraiConfig), + [JobIDs.RPR] = typeof(ReaperConfig), [JobIDs.VPR] = typeof(ViperConfig), + [JobIDs.BRD] = typeof(BardConfig), [JobIDs.MCH] = typeof(MachinistConfig), + [JobIDs.DNC] = typeof(DancerConfig), [JobIDs.BLM] = typeof(BlackMageConfig), + [JobIDs.SMN] = typeof(SummonerConfig), [JobIDs.RDM] = typeof(RedMageConfig), + [JobIDs.BLU] = typeof(BlueMageConfig), [JobIDs.PCT] = typeof(PictomancerConfig), + }; + + private static bool IsCurrentJobHudEnabled() + { + var player = Plugin.ObjectTable.LocalPlayer; + if (player == null) return false; + if (!_jobIdToConfigType.TryGetValue(player.ClassJob.RowId, out var configType)) + return false; + var config = ConfigurationManager.Instance?.GetConfigObjectForType(configType) as JobConfig; + return config?.Enabled ?? false; + } + + private static IEnumerable GetCurrentJobGaugeAddonNames() + { + var player = Plugin.ObjectTable.LocalPlayer; + if (player == null) yield break; + if (!JobsHelper.JobNames.TryGetValue(player.ClassJob.RowId, out var jobName)) + yield break; + for (int i = 0; i < 4; i++) + { + string addonName = $"JobHud{jobName}{i}"; + if (_specialCases.TryGetValue(addonName, out var name) && name != null) + addonName = name; + yield return addonName; + } + } + + private bool _hidingNameplates = false; + private bool _nameplateVisibilityBeforeHide = true; + + private unsafe void UpdateDefaultNameplates(bool forceVisible = false) + { + var nameplatesConfig = ConfigurationManager.Instance?.GetConfigObject(); + bool shouldHide = !forceVisible && Config.HideDefaultHudWhenReplaced && (nameplatesConfig?.Enabled ?? false); + + var addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("NamePlate", 1).Address; + if (addon == null) return; + + if (shouldHide) + { + if (!_hidingNameplates) + { + _nameplateVisibilityBeforeHide = addon->IsVisible; + _hidingNameplates = true; + } + addon->IsVisible = false; + } + else + { + if (_hidingNameplates) + { + addon->IsVisible = _nameplateVisibilityBeforeHide; + _hidingNameplates = false; + } + } + } + + private static Dictionary _specialCases = new() + { + ["JobHudPCT0"] = "JobHudRPM0", + ["JobHudPCT1"] = "JobHudRPM1", + + ["JobHudNIN1"] = "JobHudNIN1v70", + + ["JobHudVPR0"] = "JobHudRDB0", + ["JobHudVPR1"] = "JobHudRDB1" + }; + + private unsafe void UpdateJobGauges(bool forceVisible = false) + { + IPlayerCharacter? player = Plugin.ObjectTable.LocalPlayer; + if (player == null) { return; } + + string jobName = JobsHelper.JobNames[player.ClassJob.RowId]; + int i = 0; + bool stop = false; + + do + { + string addonName = $"JobHud{jobName}{i}"; + if (_specialCases.TryGetValue(addonName, out string? name) && name != null) + { + addonName = name; + } + + AtkUnitBase* addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName(addonName, 1).Address; + if (addon == null) + { + stop = true; + } + else + { + addon->IsVisible = forceVisible || !Config.HideDefaultJobGauges; + } + + i++; + } while (!stop); + } + } + + internal class StrataLevelComparer : IComparer where TKey : PluginConfigObject + { + public int Compare(TKey? a, TKey? b) + { + MovablePluginConfigObject? configA = a is MovablePluginConfigObject ? a as MovablePluginConfigObject : null; + MovablePluginConfigObject? configB = b is MovablePluginConfigObject ? b as MovablePluginConfigObject : null; + + if (configA == null && configB == null) { return 0; } + if (configA == null && configB != null) { return -1; } + if (configA != null && configB == null) { return 1; } + + if (configA!.StrataLevel == configB!.StrataLevel) + { + return configA.ID.CompareTo(configB.ID); + } + + if (configA.StrataLevel < configB.StrataLevel) + { + return -1; + } + + return 1; + } + } +} \ No newline at end of file diff --git a/Interface/HudManager.cs b/Interface/HudManager.cs new file mode 100644 index 0000000..d029c3d --- /dev/null +++ b/Interface/HudManager.cs @@ -0,0 +1,786 @@ +using Dalamud.Game.ClientState.Conditions; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Interface; +using Dalamud.Interface.Utility; +using HSUI.Config; +using HSUI.Helpers; +using HSUI.Interface.EnemyList; +using HSUI.Interface.GeneralElements; +using HSUI.Interface.Jobs; +using HSUI.Interface.Nameplates; +using HSUI.Interface.Party; +using HSUI.Interface.PartyCooldowns; +using HSUI.Interface.StatusEffects; +using FFXIVClientStructs.FFXIV.Component.GUI; +using Dalamud.Bindings.ImGui; +using System; +using System.Collections.Generic; +using System.Numerics; + +namespace HSUI.Interface +{ + public class HudManager : IDisposable + { + private GridConfig? _gridConfig; + private HUDOptionsConfig? _hudOptions; + private DraggableHudElement? _selectedElement = null; + + private SortedList _hudElements = null!; + private List _hudElementsUsingPlayer = null!; + private List _hudElementsUsingTarget = null!; + private List _hudElementsUsingTargetOfTarget = null!; + private List _hudElementsUsingFocusTarget = null!; + private List _hudElementsWithPreview = null!; + + private UnitFrameHud _playerUnitFrameHud = null!; + private UnitFrameHud _targetUnitFrameHud = null!; + private UnitFrameHud _totUnitFrameHud = null!; + private UnitFrameHud _focusTargetUnitFrameHud = null!; + + private PlayerCastbarHud _playerCastbarHud = null!; + private CustomEffectsListHud _customEffectsHud = null!; + private PrimaryResourceHud _playerManaBarHud = null!; + private JobHud? _jobHud = null; + + private Dictionary _jobsMap = null!; + private Dictionary _unsupportedJobsMap = null!; + private List _jobTypes = null!; + + private NameplatesHud _nameplatesHud = null!; + + private double _occupiedInQuestStartTime = -1; + + private HudHelper _hudHelper = new HudHelper(); + + public HudManager() + { + CreateJobsMap(); + + ConfigurationManager.Instance.ResetEvent += OnConfigReset; + ConfigurationManager.Instance.LockEvent += OnHUDLockChanged; + ConfigurationManager.Instance.ConfigClosedEvent += OnConfingWindowClosed; + ConfigurationManager.Instance.StrataLevelsChangedEvent += OnStrataLevelsChanged; + ConfigurationManager.Instance.GlobalVisibilityEvent += OnGlobalVisibilityChanged; + + CreateHudElements(); + } + + ~HudManager() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + + _hudHelper.Dispose(); + + foreach (var element in _hudElements.Values) + { + try { element.Dispose(); } + catch (Exception ex) { Plugin.Logger.Error($"Error disposing HUD element {element.ID}: {ex.Message}"); } + } + try { _jobHud?.Dispose(); } + catch (Exception ex) { Plugin.Logger.Error($"Error disposing JobHud: {ex.Message}"); } + + _hudElements.Clear(); + _hudElementsUsingPlayer.Clear(); + _hudElementsUsingTarget.Clear(); + _hudElementsUsingTargetOfTarget.Clear(); + _hudElementsUsingFocusTarget.Clear(); + + ConfigurationManager.Instance.ResetEvent -= OnConfigReset; + ConfigurationManager.Instance.LockEvent -= OnHUDLockChanged; + ConfigurationManager.Instance.ConfigClosedEvent -= OnConfingWindowClosed; + ConfigurationManager.Instance.StrataLevelsChangedEvent -= OnStrataLevelsChanged; + ConfigurationManager.Instance.GlobalVisibilityEvent -= OnGlobalVisibilityChanged; + } + + private void OnConfigReset(ConfigurationManager sender) + { + CreateHudElements(); + _jobHud = null; + } + + private void OnHUDLockChanged(ConfigurationManager sender) + { + var draggingEnabled = !sender.LockHUD; + + foreach (var element in _hudElements.Values) + { + element.DraggingEnabled = draggingEnabled; + element.Selected = false; + } + + if (_jobHud != null) + { + _jobHud.DraggingEnabled = draggingEnabled; + } + + _selectedElement = null; + } + + private void OnConfingWindowClosed(ConfigurationManager sender) + { + if (_hudOptions == null || !_hudOptions.AutomaticPreviewDisabling) + { + return; + } + + foreach (IHudElementWithPreview element in _hudElementsWithPreview) + { + element.StopPreview(); + } + + _nameplatesHud.StopPreview(); + } + + private void OnStrataLevelsChanged(ConfigurationManager sender, PluginConfigObject config) + { + SortedList tmp = new SortedList(new StrataLevelComparer()); + + foreach (DraggableHudElement element in _hudElements.Values) + { + tmp.Add(element.GetConfig(), element); + } + + _hudElements = tmp; + } + + private void OnGlobalVisibilityChanged(ConfigurationManager sender, VisibilityConfig config) + { + foreach (DraggableHudElement element in _hudElements.Values) + { + if (element is IHudElementWithVisibilityConfig e) + { + e.VisibilityConfig?.CopyFrom(config); + } + } + + foreach (Type jobType in _jobTypes) + { + JobConfig jobConfig = (JobConfig)ConfigurationManager.Instance.GetConfigObjectForType(jobType); + jobConfig.VisibilityConfig.CopyFrom(config); + } + } + + private void OnDraggableElementSelected(DraggableHudElement sender) + { + foreach (var element in _hudElements.Values) + { + element.Selected = element == sender; + } + + if (_jobHud != null) + { + _jobHud.Selected = _jobHud == sender; + } + + _selectedElement = sender; + } + + private void CreateHudElements() + { + _gridConfig = ConfigurationManager.Instance.GetConfigObject(); + _hudOptions = ConfigurationManager.Instance.GetConfigObject(); + + _hudElements = new SortedList(new StrataLevelComparer()); + _hudElementsUsingPlayer = new List(); + _hudElementsUsingTarget = new List(); + _hudElementsUsingTargetOfTarget = new List(); + _hudElementsUsingFocusTarget = new List(); + _hudElementsWithPreview = new List(); + + _nameplatesHud = new NameplatesHud(ConfigurationManager.Instance.GetConfigObject()); + + CreateUnitFrames(); + CreateManaBars(); + CreateCastbars(); + CreateStatusEffectsLists(); + CreateMiscElements(); + + foreach (var element in _hudElements.Values) + { + element.SelectEvent += OnDraggableElementSelected; + } + } + + private void CreateUnitFrames() + { + var playerUnitFrameConfig = ConfigurationManager.Instance.GetConfigObject(); + _playerUnitFrameHud = new PlayerUnitFrameHud(playerUnitFrameConfig, "Player"); + _hudElements.Add(playerUnitFrameConfig, _playerUnitFrameHud); + _hudElementsUsingPlayer.Add(_playerUnitFrameHud); + _hudElementsWithPreview.Add(_playerUnitFrameHud); + + var targetUnitFrameConfig = ConfigurationManager.Instance.GetConfigObject(); + _targetUnitFrameHud = new UnitFrameHud(targetUnitFrameConfig, "Target"); + _hudElements.Add(targetUnitFrameConfig, _targetUnitFrameHud); + _hudElementsUsingTarget.Add(_targetUnitFrameHud); + _hudElementsWithPreview.Add(_targetUnitFrameHud); + + var targetOfTargetUnitFrameConfig = ConfigurationManager.Instance.GetConfigObject(); + _totUnitFrameHud = new UnitFrameHud(targetOfTargetUnitFrameConfig, "Target of Target"); + _hudElements.Add(targetOfTargetUnitFrameConfig, _totUnitFrameHud); + _hudElementsUsingTargetOfTarget.Add(_totUnitFrameHud); + _hudElementsWithPreview.Add(_totUnitFrameHud); + + var focusTargetUnitFrameConfig = ConfigurationManager.Instance.GetConfigObject(); + _focusTargetUnitFrameHud = new UnitFrameHud(focusTargetUnitFrameConfig, "Focus Target"); + _hudElements.Add(focusTargetUnitFrameConfig, _focusTargetUnitFrameHud); + _hudElementsUsingFocusTarget.Add(_focusTargetUnitFrameHud); + _hudElementsWithPreview.Add(_focusTargetUnitFrameHud); + + var partyFramesConfig = ConfigurationManager.Instance.GetConfigObject(); + var partyFramesHud = new PartyFramesHud(partyFramesConfig, "Party Frames"); + _hudElements.Add(partyFramesConfig, partyFramesHud); + _hudElementsWithPreview.Add(partyFramesHud); + + var enemyListConfig = ConfigurationManager.Instance.GetConfigObject(); + var enemyListHud = new EnemyListHud(enemyListConfig, "Enemy List"); + _hudElements.Add(enemyListConfig, enemyListHud); + _hudElementsWithPreview.Add(enemyListHud); + } + + private void CreateManaBars() + { + var playerManaBarConfig = ConfigurationManager.Instance.GetConfigObject(); + _playerManaBarHud = new PrimaryResourceHud(playerManaBarConfig, "Player Mana Bar"); + _playerManaBarHud.ParentConfig = _playerUnitFrameHud.Config; + _hudElements.Add(playerManaBarConfig, _playerManaBarHud); + _hudElementsUsingPlayer.Add(_playerManaBarHud); + + var targetManaBarConfig = ConfigurationManager.Instance.GetConfigObject(); + var targetManaBarHud = new PrimaryResourceHud(targetManaBarConfig, "Target Mana Bar"); + targetManaBarHud.ParentConfig = _targetUnitFrameHud.Config; + _hudElements.Add(targetManaBarConfig, targetManaBarHud); + _hudElementsUsingTarget.Add(targetManaBarHud); + + var totManaBarConfig = ConfigurationManager.Instance.GetConfigObject(); + var totManaBarHud = new PrimaryResourceHud(totManaBarConfig, "ToT Mana Bar"); + totManaBarHud.ParentConfig = _totUnitFrameHud.Config; + _hudElements.Add(totManaBarConfig, totManaBarHud); + _hudElementsUsingTargetOfTarget.Add(totManaBarHud); + + var focusManaBarConfig = ConfigurationManager.Instance.GetConfigObject(); + var focusManaBarHud = new PrimaryResourceHud(focusManaBarConfig, "Focus Mana Bar"); + focusManaBarHud.ParentConfig = _focusTargetUnitFrameHud.Config; + _hudElements.Add(focusManaBarConfig, focusManaBarHud); + _hudElementsUsingFocusTarget.Add(focusManaBarHud); + } + + private void CreateCastbars() + { + var playerCastbarConfig = ConfigurationManager.Instance.GetConfigObject(); + _playerCastbarHud = new PlayerCastbarHud(playerCastbarConfig, "Player Castbar"); + _playerCastbarHud.ParentConfig = _playerUnitFrameHud.Config; + _hudElements.Add(playerCastbarConfig, _playerCastbarHud); + _hudElementsUsingPlayer.Add(_playerCastbarHud); + _hudElementsWithPreview.Add(_playerCastbarHud); + + var targetCastbarConfig = ConfigurationManager.Instance.GetConfigObject(); + var targetCastbar = new TargetCastbarHud(targetCastbarConfig, "Target Castbar"); + targetCastbar.ParentConfig = _targetUnitFrameHud.Config; + _hudElements.Add(targetCastbarConfig, targetCastbar); + _hudElementsUsingTarget.Add(targetCastbar); + _hudElementsWithPreview.Add(targetCastbar); + + var targetOfTargetCastbarConfig = ConfigurationManager.Instance.GetConfigObject(); + var targetOfTargetCastbar = new TargetOfTargetCastbarHud(targetOfTargetCastbarConfig, "ToT Castbar"); + targetOfTargetCastbar.ParentConfig = _totUnitFrameHud.Config; + _hudElements.Add(targetOfTargetCastbarConfig, targetOfTargetCastbar); + _hudElementsUsingTargetOfTarget.Add(targetOfTargetCastbar); + _hudElementsWithPreview.Add(targetOfTargetCastbar); + + var focusTargetCastbarConfig = ConfigurationManager.Instance.GetConfigObject(); + var focusTargetCastbar = new FocusTargetCastbarHud(focusTargetCastbarConfig, "Focus Castbar"); + focusTargetCastbar.ParentConfig = _focusTargetUnitFrameHud.Config; + _hudElements.Add(focusTargetCastbarConfig, focusTargetCastbar); + _hudElementsUsingFocusTarget.Add(focusTargetCastbar); + _hudElementsWithPreview.Add(focusTargetCastbar); + } + + private void CreateStatusEffectsLists() + { + var playerBuffsConfig = ConfigurationManager.Instance.GetConfigObject(); + var playerBuffs = new StatusEffectsListHud(playerBuffsConfig, "Buffs"); + playerBuffs.ParentConfig = _playerUnitFrameHud.Config; + _hudElements.Add(playerBuffsConfig, playerBuffs); + _hudElementsUsingPlayer.Add(playerBuffs); + _hudElementsWithPreview.Add(playerBuffs); + + var playerDebuffsConfig = ConfigurationManager.Instance.GetConfigObject(); + var playerDebuffs = new StatusEffectsListHud(playerDebuffsConfig, "Debuffs"); + playerDebuffs.ParentConfig = _playerUnitFrameHud.Config; + _hudElements.Add(playerDebuffsConfig, playerDebuffs); + _hudElementsUsingPlayer.Add(playerDebuffs); + _hudElementsWithPreview.Add(playerDebuffs); + + var targetBuffsConfig = ConfigurationManager.Instance.GetConfigObject(); + var targetBuffs = new StatusEffectsListHud(targetBuffsConfig, "Target Buffs"); + targetBuffs.ParentConfig = _targetUnitFrameHud.Config; + _hudElements.Add(targetBuffsConfig, targetBuffs); + _hudElementsUsingTarget.Add(targetBuffs); + _hudElementsWithPreview.Add(targetBuffs); + + var targetDebuffsConfig = ConfigurationManager.Instance.GetConfigObject(); + var targetDebuffs = new StatusEffectsListHud(targetDebuffsConfig, "Target Debuffs"); + targetDebuffs.ParentConfig = _targetUnitFrameHud.Config; + _hudElements.Add(targetDebuffsConfig, targetDebuffs); + _hudElementsUsingTarget.Add(targetDebuffs); + _hudElementsWithPreview.Add(targetDebuffs); + + var focusTargetBuffsConfig = ConfigurationManager.Instance.GetConfigObject(); + var focusTargetBuffs = new StatusEffectsListHud(focusTargetBuffsConfig, "focusTarget Buffs"); + focusTargetBuffs.ParentConfig = _focusTargetUnitFrameHud.Config; + _hudElements.Add(focusTargetBuffsConfig, focusTargetBuffs); + _hudElementsUsingFocusTarget.Add(focusTargetBuffs); + _hudElementsWithPreview.Add(focusTargetBuffs); + + var focusTargetDebuffsConfig = ConfigurationManager.Instance.GetConfigObject(); + var focusTargetDebuffs = new StatusEffectsListHud(focusTargetDebuffsConfig, "focusTarget Debuffs"); + focusTargetDebuffs.ParentConfig = _focusTargetUnitFrameHud.Config; + _hudElements.Add(focusTargetDebuffsConfig, focusTargetDebuffs); + _hudElementsUsingFocusTarget.Add(focusTargetDebuffs); + _hudElementsWithPreview.Add(focusTargetDebuffs); + + var custonEffectsConfig = ConfigurationManager.Instance.GetConfigObject(); + _customEffectsHud = new CustomEffectsListHud(custonEffectsConfig, "Custom Effects"); + _hudElements.Add(custonEffectsConfig, _customEffectsHud); + _hudElementsUsingPlayer.Add(_customEffectsHud); + _hudElementsWithPreview.Add(_customEffectsHud); + } + + private void CreateMiscElements() + { + var gcdIndicatorConfig = ConfigurationManager.Instance.GetConfigObject(); + var gcdIndicator = new GCDIndicatorHud(gcdIndicatorConfig, "GCD Indicator"); + _hudElements.Add(gcdIndicatorConfig, gcdIndicator); + _hudElementsUsingPlayer.Add(gcdIndicator); + + var mpTickerConfig = ConfigurationManager.Instance.GetConfigObject(); + var mpTicker = new MPTickerHud(mpTickerConfig, "MP Ticker"); + _hudElements.Add(mpTickerConfig, mpTicker); + _hudElementsUsingPlayer.Add(mpTicker); + + var expBarConfig = ConfigurationManager.Instance.GetConfigObject(); + var expBarHud = new ExperienceBarHud(expBarConfig, "Experience Bar"); + _hudElements.Add(expBarConfig, expBarHud); + _hudElementsUsingPlayer.Add(expBarHud); + + var pullTimerConfig = ConfigurationManager.Instance.GetConfigObject(); + var pullTimerHud = new PullTimerHud(pullTimerConfig, "Pull Timer"); + _hudElements.Add(pullTimerConfig, pullTimerHud); + _hudElementsUsingPlayer.Add(pullTimerHud); + + var limitBreakConfig = ConfigurationManager.Instance.GetConfigObject(); + var limitBreakHud = new LimitBreakHud(limitBreakConfig, "Limit Break"); + _hudElements.Add(limitBreakConfig, limitBreakHud); + + var hotbarsConfig = ConfigurationManager.Instance.GetConfigObject(); + HotbarBarConfig[] hotbarConfigs = new HotbarBarConfig[] + { + ConfigurationManager.Instance.GetConfigObject(), + ConfigurationManager.Instance.GetConfigObject(), + ConfigurationManager.Instance.GetConfigObject(), + ConfigurationManager.Instance.GetConfigObject(), + ConfigurationManager.Instance.GetConfigObject(), + ConfigurationManager.Instance.GetConfigObject(), + ConfigurationManager.Instance.GetConfigObject(), + ConfigurationManager.Instance.GetConfigObject(), + ConfigurationManager.Instance.GetConfigObject(), + ConfigurationManager.Instance.GetConfigObject() + }; + for (int i = 0; i < hotbarConfigs.Length; i++) + { + var barConfig = hotbarConfigs[i]; + var barHud = new ActionBarsHud(barConfig, $"Hotbar {i + 1}"); + _hudElements.Add(barConfig, barHud); + _hudElementsUsingPlayer.Add(barHud); + } + + var partyCooldownsConfig = ConfigurationManager.Instance.GetConfigObject(); + var partyCooldownsHud = new PartyCooldownsHud(partyCooldownsConfig, "Party Cooldowns"); + _hudElements.Add(partyCooldownsConfig, partyCooldownsHud); + _hudElementsWithPreview.Add(partyCooldownsHud); + } + + public void Draw(uint jobId) + { + if (!FontsManager.Instance.DefaultFontBuilt) + { + Plugin.UiBuilder.FontAtlas.BuildFontsAsync(); + } + + try + { + PullTimerHelper.Instance.Update(); + } + catch { } + + TooltipsHelper.Instance.RemoveTooltip(); // remove tooltip from previous frame + + _hudHelper.Update(); + + if (!ShouldBeVisible()) + { + return; + } + + WhosTalkingHelper.Instance?.Update(); + + ClipRectsHelper.Instance.Update(); + + ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0); + ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(0, 0)); + ImGui.PushStyleVar(ImGuiStyleVar.WindowBorderSize, 0); + + ImGuiHelpers.ForceNextWindowMainViewport(); + ImGui.SetNextWindowPos(Vector2.Zero); + ImGui.SetNextWindowSize(ImGui.GetMainViewport().Size); + + var begin = ImGui.Begin( + "HSUI_HUD", + ImGuiWindowFlags.NoTitleBar + | ImGuiWindowFlags.NoScrollbar + | ImGuiWindowFlags.AlwaysAutoResize + | ImGuiWindowFlags.NoBackground + | ImGuiWindowFlags.NoInputs + | ImGuiWindowFlags.NoBringToFrontOnFocus + | ImGuiWindowFlags.NoFocusOnAppearing + | ImGuiWindowFlags.NoSavedSettings + ); + + ImGui.PopStyleVar(3); + + if (!begin) + { + ImGui.End(); + return; + } + + UpdateJob(jobId); + AssignActors(); + + var origin = ImGui.GetMainViewport().Size / 2f; + if (_hudOptions != null && _hudOptions.UseGlobalHudShift) + { + origin += _hudOptions.HudOffset; + } + + // show only castbar during quest events + if (ShouldOnlyShowCastbar()) + { + _playerCastbarHud?.PrepareForDraw(origin); + _playerCastbarHud?.Draw(origin); + + ImGui.End(); + return; + } + + // grid + if (_gridConfig is not null && _gridConfig.Enabled) + { + DraggablesHelper.DrawGrid(_gridConfig, _hudOptions, _selectedElement); + } + + // nameplates + if (_nameplatesHud.GetConfig().Enabled) + { + ClipRectsHelper.Instance?.AddNameplatesClipRects(); + + _nameplatesHud.PrepareForDraw(origin); + _nameplatesHud.Draw(origin); + + ClipRectsHelper.Instance?.RemoveNameplatesClipRects(); + } + + // draw elements + lock (_hudElements) + { + DraggablesHelper.DrawElements(origin, _hudHelper, _hudElements.Values, _jobHud, _selectedElement); + } + + // tooltip + TooltipsHelper.Instance.Draw(); + + ImGui.End(); + } + + protected unsafe bool ShouldBeVisible() + { + if (!ConfigurationManager.Instance.ShowHUD || Plugin.ObjectTable.LocalPlayer == null) + { + return false; + } + + bool hudHidden = + Plugin.Condition[ConditionFlag.WatchingCutscene] || + Plugin.Condition[ConditionFlag.WatchingCutscene78] || + Plugin.Condition[ConditionFlag.OccupiedInCutSceneEvent] || + Plugin.Condition[ConditionFlag.CreatingCharacter] || + Plugin.Condition[ConditionFlag.BetweenAreas] || + Plugin.Condition[ConditionFlag.BetweenAreas51] || + Plugin.Condition[ConditionFlag.OccupiedSummoningBell]; + + if (hudHidden) + { + return false; + } + + AtkUnitBase* parameterWidget = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("_ParameterWidget", 1).Address; + AtkUnitBase* fadeMiddleWidget = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("FadeMiddle", 1).Address; + + bool paramenterVisible = parameterWidget != null && parameterWidget->IsVisible; + bool fadeMiddleVisible = fadeMiddleWidget != null && fadeMiddleWidget->IsVisible; + + return paramenterVisible && !fadeMiddleVisible; + } + + protected bool ShouldOnlyShowCastbar() + { + // when in quest dialogs and events, hide everything except castbars + // this includes talking to npcs or interacting with quest related stuff + if (Plugin.Condition[ConditionFlag.OccupiedInQuestEvent] || + Plugin.Condition[ConditionFlag.OccupiedInEvent]) + { + // we have to wait a bit to avoid weird flickering when clicking shiny stuff + // we hide HSUI after half a second passed in this state + // interestingly enough, default hotbars seem to do something similar + var time = ImGui.GetTime(); + if (_occupiedInQuestStartTime > 0) + { + if (time - _occupiedInQuestStartTime > 0.5) + { + return true; + } + } + else + { + _occupiedInQuestStartTime = time; + } + } + else + { + _occupiedInQuestStartTime = -1; + } + + return false; + } + + private void UpdateJob(uint newJobId) + { + if (_jobHud != null && _jobHud.Config.JobId == newJobId) + { + return; + } + + JobConfig? config = null; + + // unsupported jobs + if (_unsupportedJobsMap.ContainsKey(newJobId) && _unsupportedJobsMap.TryGetValue(newJobId, out var type)) + { + config = (JobConfig)Activator.CreateInstance(type)!; + _jobHud = new JobHud(config); + } + + // supported jobs + if (_jobsMap.TryGetValue(newJobId, out var types)) + { + config = (JobConfig)ConfigurationManager.Instance.GetConfigObjectForType(types.ConfigType); + _jobHud = (JobHud)Activator.CreateInstance(types.HudType, config, types.DisplayName)!; + _jobHud.SelectEvent += OnDraggableElementSelected; + } + } + + private void AssignActors() + { + // player + IPlayerCharacter? player = Plugin.ObjectTable.LocalPlayer; + foreach (var element in _hudElementsUsingPlayer) + { + element.Actor = player; + + if (_jobHud != null) + { + _jobHud.Actor = player; + } + } + + // target + IGameObject? target = Plugin.TargetManager.SoftTarget ?? Plugin.TargetManager.Target; + foreach (var element in _hudElementsUsingTarget) + { + element.Actor = target; + + if (_customEffectsHud != null) + { + _customEffectsHud.TargetActor = target; + } + } + + // target of target + IGameObject? targetOfTarget = Utils.FindTargetOfTarget(target, player, Plugin.ObjectTable); + foreach (var element in _hudElementsUsingTargetOfTarget) + { + element.Actor = targetOfTarget; + } + + // focus + IGameObject? focusTarget = Plugin.TargetManager.FocusTarget; + foreach (var element in _hudElementsUsingFocusTarget) + { + element.Actor = focusTarget; + } + + // player mana bar + if (_jobHud != null && _playerManaBarHud != null && !_jobHud.Config.UseDefaultPrimaryResourceBar) + { + _playerManaBarHud.ResourceType = PrimaryResourceTypes.None; + } + } + + protected void CreateJobsMap() + { + _jobsMap = new Dictionary() + { + // tanks + [JobIDs.PLD] = new JobHudTypes(typeof(PaladinHud), typeof(PaladinConfig), "Paladin HUD"), + [JobIDs.WAR] = new JobHudTypes(typeof(WarriorHud), typeof(WarriorConfig), "Warrior HUD"), + [JobIDs.DRK] = new JobHudTypes(typeof(DarkKnightHud), typeof(DarkKnightConfig), "Dark Knight HUD"), + [JobIDs.GNB] = new JobHudTypes(typeof(GunbreakerHud), typeof(GunbreakerConfig), "Gunbreaker HUD"), + + // healers + [JobIDs.WHM] = new JobHudTypes(typeof(WhiteMageHud), typeof(WhiteMageConfig), "White Mage HUD"), + [JobIDs.SCH] = new JobHudTypes(typeof(ScholarHud), typeof(ScholarConfig), "Scholar HUD"), + [JobIDs.AST] = new JobHudTypes(typeof(AstrologianHud), typeof(AstrologianConfig), "Astrologian HUD"), + [JobIDs.SGE] = new JobHudTypes(typeof(SageHud), typeof(SageConfig), "Sage HUD"), + + // melee + [JobIDs.MNK] = new JobHudTypes(typeof(MonkHud), typeof(MonkConfig), "Monk HUD"), + [JobIDs.DRG] = new JobHudTypes(typeof(DragoonHud), typeof(DragoonConfig), "Dragoon HUD"), + [JobIDs.NIN] = new JobHudTypes(typeof(NinjaHud), typeof(NinjaConfig), "Ninja HUD"), + [JobIDs.SAM] = new JobHudTypes(typeof(SamuraiHud), typeof(SamuraiConfig), "Samurai HUD"), + [JobIDs.RPR] = new JobHudTypes(typeof(ReaperHud), typeof(ReaperConfig), "Reaper HUD"), + [JobIDs.VPR] = new JobHudTypes(typeof(ViperHud), typeof(ViperConfig), "Viper HUD"), + + // ranged + [JobIDs.BRD] = new JobHudTypes(typeof(BardHud), typeof(BardConfig), "Bard HUD"), + [JobIDs.MCH] = new JobHudTypes(typeof(MachinistHud), typeof(MachinistConfig), "Mechanic HUD"), + [JobIDs.DNC] = new JobHudTypes(typeof(DancerHud), typeof(DancerConfig), "Dancer HUD"), + + // casters + [JobIDs.BLM] = new JobHudTypes(typeof(BlackMageHud), typeof(BlackMageConfig), "Black Mage HUD"), + [JobIDs.SMN] = new JobHudTypes(typeof(SummonerHud), typeof(SummonerConfig), "Summoner HUD"), + [JobIDs.RDM] = new JobHudTypes(typeof(RedMageHud), typeof(RedMageConfig), "Red Mage HUD"), + [JobIDs.BLU] = new JobHudTypes(typeof(BlueMageHud), typeof(BlueMageConfig), "Blue Mage HUD"), + [JobIDs.PCT] = new JobHudTypes(typeof(PictomancerHud), typeof(PictomancerConfig), "Pictomancer HUD") + }; + + _unsupportedJobsMap = new Dictionary() + { + // base jobs + [JobIDs.GLA] = typeof(GladiatorConfig), + [JobIDs.MRD] = typeof(MarauderConfig), + [JobIDs.PGL] = typeof(PugilistConfig), + [JobIDs.LNC] = typeof(LancerConfig), + [JobIDs.ROG] = typeof(RogueConfig), + [JobIDs.ARC] = typeof(ArcherConfig), + [JobIDs.THM] = typeof(ThaumaturgeConfig), + [JobIDs.ACN] = typeof(ArcanistConfig), + [JobIDs.CNJ] = typeof(ConjurerConfig), + + // crafters + [JobIDs.CRP] = typeof(CarpenterConfig), + [JobIDs.BSM] = typeof(BlacksmithConfig), + [JobIDs.ARM] = typeof(ArmorerConfig), + [JobIDs.GSM] = typeof(GoldsmithConfig), + [JobIDs.LTW] = typeof(LeatherworkerConfig), + [JobIDs.WVR] = typeof(WeaverConfig), + [JobIDs.ALC] = typeof(AlchemistConfig), + [JobIDs.CUL] = typeof(CulinarianConfig), + + // gatherers + [JobIDs.MIN] = typeof(MinerConfig), + [JobIDs.BOT] = typeof(BotanistConfig), + [JobIDs.FSH] = typeof(FisherConfig) + }; + + _jobTypes = new List() + { + typeof(PaladinConfig), + typeof(WarriorConfig), + typeof(DarkKnightConfig), + typeof(GunbreakerConfig), + + typeof(WhiteMageConfig), + typeof(ScholarConfig), + typeof(AstrologianConfig), + typeof(SageConfig), + + typeof(MonkConfig), + typeof(DragoonConfig), + typeof(NinjaConfig), + typeof(SamuraiConfig), + typeof(ReaperConfig), + typeof(ViperConfig), + + typeof(BardConfig), + typeof(MachinistConfig), + typeof(DancerConfig), + + typeof(BlackMageConfig), + typeof(SummonerConfig), + typeof(RedMageConfig), + typeof(BlueMageConfig), + typeof(PictomancerConfig) + }; + } + } + + internal struct JobHudTypes + { + public Type HudType; + public Type ConfigType; + public string DisplayName; + + public JobHudTypes(Type hudType, Type configType, string displayName) + { + HudType = hudType; + ConfigType = configType; + DisplayName = displayName; + } + } + + public static class ForcedJob + { + internal static bool Enabled; + internal static uint ForcedJobId; + } + + internal static class HUDConstants + { + internal static int BaseHUDOffsetY = (int)(ImGui.GetMainViewport().Size.Y * 0.3f); + internal static int UnitFramesOffsetX = 160; + internal static int PlayerCastbarY = BaseHUDOffsetY - 13; + internal static int JobHudsBaseY = PlayerCastbarY - 14; + + internal static Vector2 DefaultBigUnitFrameSize = new Vector2(270, 50); + internal static Vector2 DefaultSmallUnitFrameSize = new Vector2(120, 20); + internal static Vector2 DefaultStatusEffectsListSize = new Vector2(292, 82); + + internal static Vector2 DefaultPlayerNameplateBarSize = new Vector2(120, 10); + internal static Vector2 DefaultEnemyNameplateBarSize = new Vector2(220, 26); + } +} diff --git a/Interface/Jobs/AstrologianHud.cs b/Interface/Jobs/AstrologianHud.cs new file mode 100644 index 0000000..ad63d1a --- /dev/null +++ b/Interface/Jobs/AstrologianHud.cs @@ -0,0 +1,273 @@ +using Dalamud.Game.ClientState.JobGauge.Enums; +using Dalamud.Game.ClientState.JobGauge.Types; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Objects.Types; +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Enums; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using HSUI.Interface.GeneralElements; +using FFXIVClientStructs.FFXIV.Client.Game; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace HSUI.Interface.Jobs +{ + public class AstrologianHud : JobHud + { + private readonly SpellHelper _spellHelper = new(); + private new AstrologianConfig Config => (AstrologianConfig)_config; + private static PluginConfigColor EmptyColor => GlobalColors.Instance.EmptyColor; + + private static readonly List DotIDs = new() { 1881, 843, 838 }; + private static readonly List DotDuration = new() { 30f, 30f, 18f }; + private const float STAR_MAX_DURATION = 10f; + private const float LIGHTSPEED_MAX_DURATION = 15f; + + public AstrologianHud(JobConfig config, string? displayName = null) : base(config, displayName) + { + } + + protected override (List, List) ChildrenPositionsAndSizes() + { + List positions = new(); + List sizes = new(); + + if (Config.CardsBar.Enabled) + { + positions.Add(Config.Position + Config.CardsBar.Position); + sizes.Add(Config.CardsBar.Size); + } + + if (Config.DotBar.Enabled) + { + positions.Add(Config.Position + Config.DotBar.Position); + sizes.Add(Config.DotBar.Size); + } + + if (Config.StarBar.Enabled) + { + positions.Add(Config.Position + Config.StarBar.Position); + sizes.Add(Config.StarBar.Size); + } + + if (Config.LightspeedBar.Enabled) + { + positions.Add(Config.Position + Config.LightspeedBar.Position); + sizes.Add(Config.LightspeedBar.Size); + } + + return (positions, sizes); + } + + public override void DrawJobHud(Vector2 origin, IPlayerCharacter player) + { + Vector2 pos = origin + Config.Position; + + if (Config.CardsBar.Enabled) + { + DrawCardsBar(pos, player); + } + if (Config.DotBar.Enabled) + { + DrawDot(pos, player); + } + + if (Config.LightspeedBar.Enabled) + { + DrawLightspeed(pos, player); + } + + if (Config.StarBar.Enabled) + { + DrawStar(pos, player); + } + } + + private unsafe void DrawCardsBar(Vector2 origin, IPlayerCharacter player) + { + AstrologianCardsBarConfig config = Config.CardsBar; + PluginConfigColor emptyColor = PluginConfigColor.Empty; + + List> chunks = new(); + + uint play1 = ActionManager.Instance()->GetAdjustedActionId(37019); + PluginConfigColor play1Color = play1 == 37023 ? config.TheBalanceColor : (play1 == 37026 ? config.TheSpearColor : emptyColor); + chunks.Add(new(play1Color, 1, null)); + + uint play2 = ActionManager.Instance()->GetAdjustedActionId(37020); + PluginConfigColor play2Color = play2 == 37024 ? config.TheArrowColor : (play2 == 37027 ? config.TheBoleColor : emptyColor); + chunks.Add(new(play2Color, 1, null)); + + uint play3 = ActionManager.Instance()->GetAdjustedActionId(37021); + PluginConfigColor play3Color = play3 == 37025 ? config.TheSpireColor : (play3 == 37028 ? config.TheEwerColor : emptyColor); + chunks.Add(new(play3Color, 1, null)); + + if (player.Level >= 70) + { + uint minorArcana = ActionManager.Instance()->GetAdjustedActionId(37022); + PluginConfigColor minorArcanaColor = minorArcana == 7444 ? config.TheLordOfCrownsColor : (minorArcana == 7445 ? config.TheLadyOfCrownsColor : emptyColor); + chunks.Add(new(minorArcanaColor, 1, null)); + } + + BarHud[] bars = BarUtilities.GetChunkedBars(config, chunks.ToArray(), player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, config.StrataLevel)); + } + } + + private void DrawDot(Vector2 origin, IPlayerCharacter player) + { + IGameObject? target = Plugin.TargetManager.SoftTarget ?? Plugin.TargetManager.Target; + BarHud? bar = BarUtilities.GetDoTBar(Config.DotBar, player, target, DotIDs, DotDuration); + if (bar != null) + { + AddDrawActions(bar.GetDrawActions(origin, Config.DotBar.StrataLevel)); + } + } + + private void DrawLightspeed(Vector2 origin, IPlayerCharacter player) + { + float lightspeedDuration = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 841 && o.SourceId == player.GameObjectId)?.RemainingTime ?? 0f; + + if (Config.LightspeedBar.HideWhenInactive && lightspeedDuration <= 0) + { + return; + } + + Config.LightspeedBar.Label.SetValue(lightspeedDuration); + + BarHud bar = BarUtilities.GetProgressBar(Config.LightspeedBar, lightspeedDuration, LIGHTSPEED_MAX_DURATION, 0, player); + AddDrawActions(bar.GetDrawActions(origin, Config.LightspeedBar.StrataLevel)); + } + + private void DrawStar(Vector2 origin, IPlayerCharacter player) + { + float starPreCookingBuff = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 1224 && o.SourceId == player.GameObjectId)?.RemainingTime ?? 0f; + float starPostCookingBuff = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 1248 && o.SourceId == player.GameObjectId)?.RemainingTime ?? 0f; + + if (Config.StarBar.HideWhenInactive && starPostCookingBuff <= 0f && starPreCookingBuff <= 0f) + { + return; + } + + float currentStarDuration = starPreCookingBuff > 0 ? STAR_MAX_DURATION - Math.Abs(starPreCookingBuff) : Math.Abs(starPostCookingBuff); + PluginConfigColor currentStarColor = starPreCookingBuff > 0 ? Config.StarBar.StarEarthlyColor : Config.StarBar.StarGiantColor; + + Config.StarBar.Label.SetValue(currentStarDuration); + + // Star Countdown after Star is ready + BarHud bar = BarUtilities.GetProgressBar(Config.StarBar, currentStarDuration, STAR_MAX_DURATION, 0f, player, currentStarColor, Config.StarBar.StarGlowConfig.Enabled && starPostCookingBuff > 0 ? Config.StarBar.StarGlowConfig : null); + AddDrawActions(bar.GetDrawActions(origin, Config.StarBar.StrataLevel)); + } + } + + [Section("Job Specific Bars")] + [SubSection("Healer", 0)] + [SubSection("Astrologian", 1)] + public class AstrologianConfig : JobConfig + { + [JsonIgnore] public override uint JobId => JobIDs.AST; + + public new static AstrologianConfig DefaultConfig() + { + var config = new AstrologianConfig(); + config.UseDefaultPrimaryResourceBar = true; + + return config; + } + + [NestedConfig("Cards Bar", 40)] + public AstrologianCardsBarConfig CardsBar = new( + new Vector2(0, 0), + new Vector2(254, 20) + ); + + [NestedConfig("Dot Bar", 40)] + public ProgressBarConfig DotBar = new( + new Vector2(-85, -29), + new Vector2(84, 14), + new PluginConfigColor(new Vector4(20f / 255f, 80f / 255f, 168f / 255f, 255f / 100f)) + ); + + [NestedConfig("Star Bar", 45)] + public AstrologianStarBarConfig StarBar = new( + new Vector2(0, -29), + new Vector2(84, 14) + ); + + [NestedConfig("Lightspeed Bar", 50)] + public ProgressBarConfig LightspeedBar = new( + new Vector2(85, -29), + new Vector2(84, 14), + new PluginConfigColor(new Vector4(255f / 255f, 255f / 255f, 173f / 255f, 100f / 100f)) + ); + } + + [Exportable(false)] + [DisableParentSettings("FillColor", "UsePartialFillColor", "UseChunks", "PartialFillColor", "LabelMode", "HideWhenInactive")] + public class AstrologianCardsBarConfig : ChunkedBarConfig + { + [ColorEdit4("The Balance Color", spacing = true)] + [Order(201)] + public PluginConfigColor TheBalanceColor = PluginConfigColor.FromHex(0xFFBE423F); + + [ColorEdit4("The Arrow Color")] + [Order(201)] + public PluginConfigColor TheArrowColor = PluginConfigColor.FromHex(0xFF628AA7); + + [ColorEdit4("The Spire Color")] + [Order(201)] + public PluginConfigColor TheSpireColor = PluginConfigColor.FromHex(0xFFC8A348); + + [ColorEdit4("The Spear Color")] + [Order(201)] + public PluginConfigColor TheSpearColor = PluginConfigColor.FromHex(0xFF5673DF); + + [ColorEdit4("The Bole Color")] + [Order(201)] + public PluginConfigColor TheBoleColor = PluginConfigColor.FromHex(0xFF9ACB77); + + [ColorEdit4("The Ewer Color")] + [Order(201)] + public PluginConfigColor TheEwerColor = PluginConfigColor.FromHex(0xFF7FBDFF); + + [ColorEdit4("The Lord of Crowns Color")] + [Order(201)] + public PluginConfigColor TheLordOfCrownsColor = PluginConfigColor.FromHex(0xFFCA3640); + + [ColorEdit4("The Lady of Crowns Color")] + [Order(201)] + public PluginConfigColor TheLadyOfCrownsColor = PluginConfigColor.FromHex(0xFF974A97); + + public AstrologianCardsBarConfig(Vector2 position, Vector2 size) + : base(position, size, PluginConfigColor.Empty) + { + } + } + + [Exportable(false)] + [DisableParentSettings("FillColor")] + public class AstrologianStarBarConfig : ProgressBarConfig + { + [ColorEdit4("Earthly" + "##Star")] + [Order(402)] + public PluginConfigColor StarEarthlyColor = new(new Vector4(37f / 255f, 181f / 255f, 177f / 255f, 100f / 100f)); + + [ColorEdit4("Giant" + "##Star")] + [Order(403)] + public PluginConfigColor StarGiantColor = new(new Vector4(198f / 255f, 154f / 255f, 199f / 255f, 100f / 100f)); + + [NestedConfig("Giant Dominance Glow" + "##Star", 404, separator = false, spacing = true)] + public BarGlowConfig StarGlowConfig = new(); + public AstrologianStarBarConfig(Vector2 position, Vector2 size) + : base(position, size, PluginConfigColor.Empty) + { + } + } +} diff --git a/Interface/Jobs/BardHud.cs b/Interface/Jobs/BardHud.cs new file mode 100644 index 0000000..2e50ac8 --- /dev/null +++ b/Interface/Jobs/BardHud.cs @@ -0,0 +1,489 @@ +using Dalamud.Game.ClientState.JobGauge.Enums; +using Dalamud.Game.ClientState.JobGauge.Types; +using Dalamud.Game.ClientState.Objects.SubKinds; +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Enums; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using HSUI.Interface.GeneralElements; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace HSUI.Interface.Jobs +{ + public class BardHud : JobHud + { + private readonly SpellHelper _spellHelper = new(); + private new BardConfig Config => (BardConfig)_config; + private PluginConfigColor EmptyColor => GlobalColors.Instance.EmptyColor; + + public BardHud(BardConfig config, string? displayName = null) : base(config, displayName) + { + } + + protected override (List, List) ChildrenPositionsAndSizes() + { + List positions = new List(); + List sizes = new List(); + + if (Config.SongGaugeBar.Enabled) + { + positions.Add(Config.Position + Config.SongGaugeBar.Position); + sizes.Add(Config.SongGaugeBar.Size); + } + + if (Config.SoulVoiceBar.Enabled) + { + positions.Add(Config.Position + Config.SoulVoiceBar.Position); + sizes.Add(Config.SoulVoiceBar.Size); + } + + if (Config.StacksBar.Enabled) + { + positions.Add(Config.Position + Config.StacksBar.Position); + sizes.Add(Config.StacksBar.Size); + } + + if (Config.CausticBiteDoTBar.Enabled) + { + positions.Add(Config.Position + Config.CausticBiteDoTBar.Position); + sizes.Add(Config.CausticBiteDoTBar.Size); + } + + if (Config.StormbiteDoTBar.Enabled) + { + positions.Add(Config.Position + Config.StormbiteDoTBar.Position); + sizes.Add(Config.StormbiteDoTBar.Size); + } + + if (Config.CodaBar.Enabled) + { + positions.Add(Config.Position + Config.CodaBar.Position); + sizes.Add(Config.CodaBar.Size); + } + + return (positions, sizes); + } + public override void DrawJobHud(Vector2 origin, IPlayerCharacter player) + { + Vector2 pos = origin + Config.Position; + + if (Config.CausticBiteDoTBar.Enabled) + { + DrawCausticBiteDoTBar(pos, player); + } + + if (Config.StormbiteDoTBar.Enabled) + { + DrawStormbiteDoTBar(pos, player); + } + + HandleCurrentSong(pos, player); + + if (Config.SoulVoiceBar.Enabled) + { + DrawSoulVoiceBar(pos, player); + } + + if (Config.CodaBar.Enabled) + { + DrawCodaBar(pos, player); + } + } + + private void DrawCodaBar(Vector2 origin, IPlayerCharacter player) + { + BRDGauge gauge = Plugin.JobGauges.Get(); + var containsCoda = new[] { gauge.Coda.Contains(Song.Wanderer) ? 1 : 0, gauge.Coda.Contains(Song.Mage) ? 1 : 0, gauge.Coda.Contains(Song.Army) ? 1 : 0 }; + bool hasCoda = containsCoda.Any(o => o == 1); + + if (!Config.CodaBar.HideWhenInactive || hasCoda) + { + var order = Config.CodaBar.CodaOrder; + var colors = new[] { Config.CodaBar.WMColor, Config.CodaBar.MBColor, Config.CodaBar.APColor }; + + var coda = new Tuple[3]; + for (int i = 0; i < 3; i++) + { + coda[i] = new Tuple(colors[order[i]], containsCoda[order[i]], null); + } + + BarHud[] bars = BarUtilities.GetChunkedBars(Config.CodaBar, coda, player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.CodaBar.StrataLevel)); + } + } + } + + private static List CausticBiteDoTIDs = new List { 124, 1200 }; + private static List CausticBiteDoTDurations = new List { 45, 45 }; + + protected void DrawCausticBiteDoTBar(Vector2 origin, IPlayerCharacter player) + { + var target = Plugin.TargetManager.SoftTarget ?? Plugin.TargetManager.Target; + + BarHud? bar = BarUtilities.GetDoTBar(Config.CausticBiteDoTBar, player, target, CausticBiteDoTIDs, CausticBiteDoTDurations); + if (bar != null) + { + AddDrawActions(bar.GetDrawActions(origin, Config.CausticBiteDoTBar.StrataLevel)); + } + } + + private static List StormbiteDoTIDs = new List { 129, 1201 }; + private static List StormbiteDoTDurations = new List { 45, 45 }; + + protected void DrawStormbiteDoTBar(Vector2 origin, IPlayerCharacter player) + { + var target = Plugin.TargetManager.SoftTarget ?? Plugin.TargetManager.Target; + + BarHud? bar = BarUtilities.GetDoTBar(Config.StormbiteDoTBar, player, target, StormbiteDoTIDs, StormbiteDoTDurations); + if (bar != null) + { + AddDrawActions(bar.GetDrawActions(origin, Config.StormbiteDoTBar.StrataLevel)); + } + } + + private void HandleCurrentSong(Vector2 origin, IPlayerCharacter player) + { + BRDGauge gauge = Plugin.JobGauges.Get(); + byte songStacks = gauge.Repertoire; + Song song = gauge.Song; + ushort songTimer = gauge.SongTimer; + + switch (song) + { + case Song.Wanderer: + if (Config.StacksBar.Enabled && Config.StacksBar.ShowWMStacks) + { + DrawStacksBar( + origin, + player, + songStacks, + 3, + Config.StacksBar.WMStackColor, + Config.StacksBar.WMGlowConfig.Enabled && songStacks == 3 ? Config.StacksBar.WMGlowConfig : null + ); + } + + DrawSongTimerBar(origin, songTimer, Config.SongGaugeBar.WMColor, Config.SongGaugeBar.WMThreshold, player); + + break; + + case Song.Mage: + if (Config.StacksBar.Enabled && Config.StacksBar.ShowMBProc) + { + DrawBloodletterReady(origin, player); + } + + DrawSongTimerBar(origin, songTimer, Config.SongGaugeBar.MBColor, Config.SongGaugeBar.MBThreshold, player); + + break; + + case Song.Army: + if (Config.StacksBar.Enabled && Config.StacksBar.ShowAPStacks) + { + DrawStacksBar(origin, player, songStacks, 4, Config.StacksBar.APStackColor); + } + + DrawSongTimerBar(origin, songTimer, Config.SongGaugeBar.APColor, Config.SongGaugeBar.APThreshold, player); + + break; + + case Song.None: + if (Config.StacksBar.Enabled && !Config.StacksBar.HideWhenInactive) + { + DrawStacksBar(origin, player, 0, 3, Config.StacksBar.WMStackColor); + } + + DrawSongTimerBar(origin, 0, EmptyColor, Config.SongGaugeBar.ThresholdConfig, player); + + break; + + default: + if (Config.StacksBar.Enabled && !Config.StacksBar.HideWhenInactive) + { + DrawStacksBar(origin, player, 0, 3, Config.StacksBar.WMStackColor); + } + + DrawSongTimerBar(origin, 0, EmptyColor, Config.SongGaugeBar.ThresholdConfig, player); + + break; + } + } + + private void DrawBloodletterReady(Vector2 origin, IPlayerCharacter player) + { + int maxStacks = player.Level < 84 ? 2 : 3; + int maxCooldown = maxStacks * 15; + int cooldown = _spellHelper.GetSpellCooldownInt(110); + cooldown = player.Level < 84 ? Math.Max(0, cooldown - 15) : cooldown; + + int stacks = (maxCooldown - cooldown) / 15; + + DrawStacksBar(origin, player, stacks, maxStacks, Config.StacksBar.MBProcColor, + Config.StacksBar.MBGlowConfig.Enabled ? Config.StacksBar.MBGlowConfig : null); + } + + protected void DrawSongTimerBar(Vector2 origin, ushort songTimer, PluginConfigColor songColor, ThresholdConfig songThreshold, IPlayerCharacter player) + { + + if (Config.SongGaugeBar.HideWhenInactive && songTimer == 0 || !Config.SongGaugeBar.Enabled) + { + return; + } + + float duration = Math.Abs(songTimer / 1000f); + + Config.SongGaugeBar.Label.SetValue(duration); + Config.SongGaugeBar.ThresholdConfig = songThreshold; + + BarHud bar = BarUtilities.GetProgressBar(Config.SongGaugeBar, duration, 45f, 0f, player, songColor); + AddDrawActions(bar.GetDrawActions(origin, Config.SongGaugeBar.StrataLevel)); + } + + protected void DrawSoulVoiceBar(Vector2 origin, IPlayerCharacter player) + { + BardSoulVoiceBarConfig config = Config.SoulVoiceBar; + byte soulVoice = Plugin.JobGauges.Get().SoulVoice; + + if (config.HideWhenInactive && soulVoice == 0) + { + return; + } + + config.Label.SetValue(soulVoice); + + BarHud bar = BarUtilities.GetProgressBar( + config, + config.ThresholdConfig, + new LabelConfig[] { config.Label }, + soulVoice, + 100f, + 0f, + player, + config.FillColor, + soulVoice == 100f && config.GlowConfig.Enabled ? config.GlowConfig : null + ); + + AddDrawActions(bar.GetDrawActions(origin, config.StrataLevel)); + } + + private void DrawStacksBar(Vector2 origin, IPlayerCharacter player, int amount, int max, PluginConfigColor stackColor, BarGlowConfig? glowConfig = null) + { + BardStacksBarConfig config = Config.StacksBar; + + config.FillColor = stackColor; + + BarHud[] bars = BarUtilities.GetChunkedBars(Config.StacksBar, max, amount, max, 0f, player, glowConfig: glowConfig); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.CodaBar.StrataLevel)); + } + } + } + + [Section("Job Specific Bars")] + [SubSection("Ranged", 0)] + [SubSection("Bard", 1)] + public class BardConfig : JobConfig + { + [JsonIgnore] public override uint JobId => JobIDs.BRD; + + public new static BardConfig DefaultConfig() + { + var config = new BardConfig(); + + config.SoulVoiceBar.Label.FontID = FontsConfig.DefaultMediumFontKey; + + config.StormbiteDoTBar.Label.FontID = FontsConfig.DefaultMediumFontKey; + config.StormbiteDoTBar.Label.TextAnchor = DrawAnchor.Left; + config.StormbiteDoTBar.Label.FrameAnchor = DrawAnchor.Left; + config.StormbiteDoTBar.Label.Position = new Vector2(2, 0); + + config.CausticBiteDoTBar.Label.FontID = FontsConfig.DefaultMediumFontKey; + config.CausticBiteDoTBar.Label.TextAnchor = DrawAnchor.Right; + config.CausticBiteDoTBar.Label.FrameAnchor = DrawAnchor.Right; + config.CausticBiteDoTBar.Label.Position = new Vector2(-2, 0); + config.CausticBiteDoTBar.FillDirection = BarDirection.Left; + + config.SoulVoiceBar.ThresholdConfig.Enabled = true; + config.SoulVoiceBar.ThresholdConfig.Value = 80; + config.SoulVoiceBar.ThresholdConfig.ThresholdType = ThresholdType.Above; + config.SoulVoiceBar.ThresholdConfig.ChangeColor = true; + config.SoulVoiceBar.ThresholdConfig.Color = new PluginConfigColor(new Vector4(150f / 255f, 0f / 255f, 255f / 255f, 100f / 100f)); + + return config; + } + + [NestedConfig("Song Gauge Bar", 30)] + public BardSongBarConfig SongGaugeBar = new BardSongBarConfig( + new(0, -22), + new(254, 20), + new PluginConfigColor(new Vector4(255f / 255f, 255f / 255f, 255f / 255f, 0f / 100f)) + ); + + [NestedConfig("Soul Voice Bar", 35)] + public BardSoulVoiceBarConfig SoulVoiceBar = new BardSoulVoiceBarConfig( + new(0, -5), + new(254, 10), + new PluginConfigColor(new Vector4(248f / 255f, 227f / 255f, 0f / 255f, 100f / 100f)) + ); + + [NestedConfig("Stacks Bar", 40)] + public BardStacksBarConfig StacksBar = new BardStacksBarConfig( + new(0, -39), + new(254, 10), + new PluginConfigColor(new Vector4(255f / 255f, 255f / 255f, 255f / 255f, 0f / 100f)) + ); + + [NestedConfig("Caustic Bite Bar", 60)] + public ProgressBarConfig CausticBiteDoTBar = new ProgressBarConfig( + new(-64, -51), + new(126, 10), + new PluginConfigColor(new Vector4(182f / 255f, 68f / 255f, 235f / 255f, 100f / 100f)) + ); + + [NestedConfig("Stormbite Bar", 65)] + public ProgressBarConfig StormbiteDoTBar = new ProgressBarConfig( + new(64, -51), + new(126, 10), + new PluginConfigColor(new Vector4(72f / 255f, 117f / 255f, 202f / 255f, 100f / 100f)) + ); + + [NestedConfig("Coda Bar", 40)] + public BardCodaBarConfig CodaBar = new BardCodaBarConfig( + new(0, -63), + new(254, 10), + new PluginConfigColor(new Vector4(0, 0, 0, 0)) + ); + } + + [DisableParentSettings("FillColor", "ThresholdConfig")] + [Exportable(false)] + public class BardSongBarConfig : ProgressBarConfig + { + [ColorEdit4("Wanderer's Minuet" + "##Song")] + [Order(31)] + public PluginConfigColor WMColor = new(new Vector4(158f / 255f, 157f / 255f, 36f / 255f, 100f / 100f)); + + [ColorEdit4("Mage's Ballad" + "##Song")] + [Order(32)] + public PluginConfigColor MBColor = new(new Vector4(143f / 255f, 90f / 255f, 143f / 255f, 100f / 100f)); + + [ColorEdit4("Army's Paeon" + "##Song")] + [Order(33)] + public PluginConfigColor APColor = new(new Vector4(207f / 255f, 205f / 255f, 52f / 255f, 100f / 100f)); + + [NestedConfig("Wanderer's Minuet Threshold", 36, separator = false, spacing = true)] + public ThresholdConfig WMThreshold = new ThresholdConfig() + { + ChangeColor = true, + Enabled = true, + ThresholdType = ThresholdType.Below, + Value = 3 + }; + + [NestedConfig("Mage's Ballad Threshold", 37, separator = false, spacing = true)] + public ThresholdConfig MBThreshold = new ThresholdConfig() + { + ChangeColor = true, + Enabled = true, + ThresholdType = ThresholdType.Below, + Value = 14 + }; + + [NestedConfig("Army's Paeon Threshold", 38, separator = false, spacing = true)] + public ThresholdConfig APThreshold = new ThresholdConfig() + { + ChangeColor = true, + Enabled = true, + ThresholdType = ThresholdType.Below, + Value = 3 + }; + + public BardSongBarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) + : base(position, size, fillColor) + { + } + } + + [Exportable(false)] + public class BardSoulVoiceBarConfig : ProgressBarConfig + { + [NestedConfig("Show Glow", 39, separator = false, spacing = true)] + public BarGlowConfig GlowConfig = new BarGlowConfig(); + + public BardSoulVoiceBarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) + : base(position, size, fillColor) + { + } + } + + [DisableParentSettings("FillColor")] + [Exportable(false)] + public class BardStacksBarConfig : ChunkedBarConfig + { + [Checkbox("Wanderer's Minuet Stacks", separator = false, spacing = true)] + [Order(51)] + public bool ShowWMStacks = true; + + [NestedConfig("Wanderer's Minuet Stacks Glow", 52, separator = false, spacing = true)] + public BarGlowConfig WMGlowConfig = new BarGlowConfig(); + + [Checkbox("Mage's Ballad Proc" + "##Stacks")] + [Order(53)] + public bool ShowMBProc = true; + + [NestedConfig("Mage's Ballad Proc Glow", 54, separator = false, spacing = true)] + public BarGlowConfig MBGlowConfig = new BarGlowConfig(); + + [Checkbox("Army's Paeon Stacks" + "##Stacks")] + [Order(56)] + public bool ShowAPStacks = true; + + [ColorEdit4("Wanderer's Minuet Stack" + "##Stacks")] + [Order(57)] + public PluginConfigColor WMStackColor = new(new Vector4(150f / 255f, 215f / 255f, 232f / 255f, 100f / 100f)); + + [ColorEdit4("Mage's Ballad Proc" + "##Stacks")] + [Order(58)] + public PluginConfigColor MBProcColor = new(new Vector4(199f / 255f, 46f / 255f, 46f / 255f, 100f / 100f)); + + [ColorEdit4("Army's Paeon Stack" + "##Stacks")] + [Order(59)] + public PluginConfigColor APStackColor = new(new Vector4(0f / 255f, 222f / 255f, 177f / 255f, 100f / 100f)); + + public BardStacksBarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) + : base(position, size, fillColor) + { + } + } + + [DisableParentSettings("FillColor")] + [Exportable(false)] + public class BardCodaBarConfig : ChunkedBarConfig + { + [ColorEdit4("Wanderer's Minuet" + "##Coda", spacing = true)] + [Order(71)] + public PluginConfigColor WMColor = new(new Vector4(145f / 255f, 186f / 255f, 94f / 255f, 100f / 100f)); + + [ColorEdit4("Mage's Ballad" + "##Coda")] + [Order(72)] + public PluginConfigColor MBColor = new(new Vector4(143f / 255f, 90f / 255f, 143f / 255f, 100f / 100f)); + + [ColorEdit4("Army's Paeon" + "##Coda")] + [Order(73)] + public PluginConfigColor APColor = new(new Vector4(207f / 255f, 205f / 255f, 52f / 255f, 100f / 100f)); + + [DragDropHorizontal("Order", "Wanderer's Minuet", "Mage's Ballad", "Army's Paeon")] + [Order(74)] + public int[] CodaOrder = new int[] { 0, 1, 2 }; + + public BardCodaBarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) : base(position, size, fillColor, 2) { } + } +} diff --git a/Interface/Jobs/BaseJobsConfig.cs b/Interface/Jobs/BaseJobsConfig.cs new file mode 100644 index 0000000..58a9191 --- /dev/null +++ b/Interface/Jobs/BaseJobsConfig.cs @@ -0,0 +1,60 @@ +using HSUI.Helpers; +using Newtonsoft.Json; + +namespace HSUI.Interface.Jobs +{ + public class BaseJobsConfig : JobConfig + { + public override uint JobId => 0; + + public BaseJobsConfig() + { + UseDefaultPrimaryResourceBar = true; + } + } + + public class GladiatorConfig : BaseJobsConfig + { + [JsonIgnore] public override uint JobId => JobIDs.GLA; + } + + public class MarauderConfig : BaseJobsConfig + { + [JsonIgnore] public override uint JobId => JobIDs.MRD; + } + + public class PugilistConfig : BaseJobsConfig + { + [JsonIgnore] public override uint JobId => JobIDs.PGL; + } + + public class LancerConfig : BaseJobsConfig + { + [JsonIgnore] public override uint JobId => JobIDs.LNC; + } + + public class RogueConfig : BaseJobsConfig + { + [JsonIgnore] public override uint JobId => JobIDs.ROG; + } + + public class ArcherConfig : BaseJobsConfig + { + [JsonIgnore] public override uint JobId => JobIDs.ARC; + } + + public class ThaumaturgeConfig : BaseJobsConfig + { + [JsonIgnore] public override uint JobId => JobIDs.THM; + } + + public class ArcanistConfig : BaseJobsConfig + { + [JsonIgnore] public override uint JobId => JobIDs.ACN; + } + + public class ConjurerConfig : BaseJobsConfig + { + [JsonIgnore] public override uint JobId => JobIDs.CNJ; + } +} diff --git a/Interface/Jobs/BlackMageHud.cs b/Interface/Jobs/BlackMageHud.cs new file mode 100644 index 0000000..2039a6f --- /dev/null +++ b/Interface/Jobs/BlackMageHud.cs @@ -0,0 +1,588 @@ +using Dalamud.Game.ClientState.JobGauge.Types; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Objects.Types; +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Enums; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using HSUI.Interface.GeneralElements; +using FFXIVClientStructs.FFXIV.Client.Game.Gauge; +using Newtonsoft.Json; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using System.Linq; +using System.Numerics; +using System.Runtime.InteropServices; + +namespace HSUI.Interface.Jobs +{ + public class BlackMageHud : JobHud + { + private new BlackMageConfig Config => (BlackMageConfig)_config; + + private static readonly List ThunderDoTIDs = new() { 161, 162, 163, 1210, 3871, 3872 }; + private static readonly List ThunderDoTDurations = new() { 24, 18, 27, 21, 30, 24 }; + + public BlackMageHud(BlackMageConfig config, string? displayName = null) : base(config, displayName) + { + + } + + protected override (List, List) ChildrenPositionsAndSizes() + { + List positions = new List(); + List sizes = new List(); + + if (Config.ManaBar.Enabled) + { + positions.Add(Config.Position + Config.ManaBar.Position); + sizes.Add(Config.ManaBar.Size); + } + + if (Config.StacksBar.Enabled) + { + positions.Add(Config.Position + Config.StacksBar.Position); + sizes.Add(Config.StacksBar.Size); + } + + if (Config.UmbralHeartBar.Enabled) + { + positions.Add(Config.Position + Config.UmbralHeartBar.Position); + sizes.Add(Config.UmbralHeartBar.Size); + } + + if (Config.TriplecastBar.Enabled) + { + positions.Add(Config.Position + Config.TriplecastBar.Position); + sizes.Add(Config.TriplecastBar.Size); + } + + if (Config.EnochianBar.Enabled) + { + positions.Add(Config.Position + Config.EnochianBar.Position); + sizes.Add(Config.EnochianBar.Size); + } + + if (Config.PolyglotBar.Enabled) + { + positions.Add(Config.Position + Config.PolyglotBar.Position); + sizes.Add(Config.PolyglotBar.Size); + } + + if (Config.ParadoxBar.Enabled) + { + positions.Add(Config.Position + Config.ParadoxBar.Position); + sizes.Add(Config.ParadoxBar.Size); + } + + if (Config.ThunderDoTBar.Enabled && !Config.ThunderDoTBar.HideWhenInactive) + { + positions.Add(Config.Position + Config.ThunderDoTBar.Position); + sizes.Add(Config.ThunderDoTBar.Size); + } + + return (positions, sizes); + } + + public override void DrawJobHud(Vector2 origin, IPlayerCharacter player) + { + Vector2 pos = origin + Config.Position; + + if (Config.ManaBar.Enabled) + { + DrawManaBar(pos, player); + } + + if (Config.StacksBar.Enabled) + { + DrawStacksBar(pos); + } + + if (Config.UmbralHeartBar.Enabled) + { + DrawUmbralHeartBar(pos); + } + + if (Config.AstralSoulBar.Enabled) + { + DrawAstralSoulBar(pos); + } + + if (Config.TriplecastBar.Enabled) + { + DrawTripleCastBar(pos, player); + } + + if (Config.PolyglotBar.Enabled) + { + DrawPolyglotBar(pos, player); + } + + if (Config.ParadoxBar.Enabled) + { + DrawParadoxBar(pos, player); + } + + if (Config.EnochianBar.Enabled) + { + DrawEnochianBar(pos, player); + } + + if (Config.ThunderDoTBar.Enabled) + { + DrawThunderDoTBar(pos, player); + } + } + + protected unsafe void DrawManaBar(Vector2 origin, IPlayerCharacter player) + { + BlackMageManaBarConfig config = Config.ManaBar; + BLMGauge _gauge = Plugin.JobGauges.Get(); + BlackMageGaugeTmp* gauge = (BlackMageGaugeTmp*)_gauge.Address; + bool inUmbralIce = gauge->ElementStance < 0; + bool inAstralFire = gauge->ElementStance > 0; + + if (config.HideWhenInactive && !inAstralFire && !inUmbralIce && player.CurrentMp == player.MaxMp) + { + return; + } + + // value + config.ValueLabelConfig.SetValue(player.CurrentMp); + + bool drawTreshold = inAstralFire || !config.ThresholdConfig.ShowOnlyDuringAstralFire; + + PluginConfigColor fillColor = config.FillColor; + PluginConfigColor bgColor = config.BackgroundColor; + if (config.UseElementColor) + { + fillColor = inAstralFire ? config.FireColor : inUmbralIce ? config.IceColor : config.FillColor; + bgColor = inAstralFire ? config.FireBackgroundColor : inUmbralIce ? config.IceBackgroundColor : config.BackgroundColor; + } + + BarHud bar = BarUtilities.GetProgressBar( + config, + drawTreshold ? config.ThresholdConfig : null, + [config.ValueLabelConfig], + player.CurrentMp, + player.MaxMp, + 0, + player, + fillColor: fillColor, + backgroundColor: bgColor + ); + + AddDrawActions(bar.GetDrawActions(origin, config.StrataLevel)); + } + + protected unsafe void DrawStacksBar(Vector2 origin) + { + BLMGauge _gauge = Plugin.JobGauges.Get(); + BlackMageGaugeTmp* gauge = (BlackMageGaugeTmp*)_gauge.Address; + int umbralIceStacks = gauge->UmbralStacks; + int astralFireStacks = gauge->AstralStacks; + + if (Config.StacksBar.HideWhenInactive && umbralIceStacks == 0 && astralFireStacks == 0) + { + return; + }; + + PluginConfigColor color = umbralIceStacks > 0 ? Config.StacksBar.IceColor : Config.StacksBar.FireColor; + int stacks = umbralIceStacks > 0 ? umbralIceStacks : astralFireStacks; + + BarHud[] bars = BarUtilities.GetChunkedBars(Config.StacksBar, 3, stacks, 3f, fillColor: color); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.StacksBar.StrataLevel)); + } + } + + protected unsafe void DrawUmbralHeartBar(Vector2 origin) + { + BLMGauge _gauge = Plugin.JobGauges.Get(); + BlackMageGaugeTmp* gauge = (BlackMageGaugeTmp*)_gauge.Address; + + if (Config.UmbralHeartBar.HideWhenInactive && gauge->UmbralHearts == 0) + { + return; + }; + + BarHud[] bars = BarUtilities.GetChunkedBars(Config.UmbralHeartBar, 3, gauge->UmbralHearts, 3f); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.UmbralHeartBar.StrataLevel)); + } + } + + protected unsafe void DrawAstralSoulBar(Vector2 origin) + { + BLMGauge gauge = Plugin.JobGauges.Get(); + BlackMageGaugeTmp* internalGauge = (BlackMageGaugeTmp*)gauge.Address; + int stacks = internalGauge->AstralSoulStacks; + const int maxStacks = 6; + + if (Config.AstralSoulBar.HideWhenInactive && stacks == 0) + { + return; + }; + + bool isFull = stacks == maxStacks; + BarGlowConfig? glow = isFull && Config.AstralSoulBar.GlowConfig.Enabled ? Config.AstralSoulBar.GlowConfig : null; + PluginConfigColor color = isFull ? Config.AstralSoulBar.FullStacksColor : Config.AstralSoulBar.FillColor; + + BarHud[] bars = BarUtilities.GetChunkedBars(Config.AstralSoulBar, maxStacks, stacks, maxStacks, fillColor: color, glowConfig: glow); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.AstralSoulBar.StrataLevel)); + } + } + + protected void DrawTripleCastBar(Vector2 origin, IPlayerCharacter player) + { + ushort stackCount = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 1211)?.Param ?? 0; + int maxCount = 3; + + if (Config.TriplecastBar.CountSwiftcast) + { + bool hasSwiftcast = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 167) != null; + if (hasSwiftcast) + { + stackCount++; + maxCount = stackCount == 4 ? 4 : 3; + } + } + + if (Config.TriplecastBar.HideWhenInactive && stackCount == 0) + { + return; + }; + + BarHud[] bars = BarUtilities.GetChunkedBars(Config.TriplecastBar, maxCount, stackCount, maxCount); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.TriplecastBar.StrataLevel)); + } + } + + protected unsafe void DrawEnochianBar(Vector2 origin, IPlayerCharacter player) + { + BLMGauge _gauge = Plugin.JobGauges.Get(); + BlackMageGaugeTmp* gauge = (BlackMageGaugeTmp*)_gauge.Address; + + if (Config.EnochianBar.HideWhenInactive && !gauge->EnochianActive) + { + return; + } + + float timer = gauge->EnochianActive ? (30000f - gauge->EnochianTimer) : 0f; + Config.EnochianBar.Label.SetValue(timer / 1000); + + BarHud bar = BarUtilities.GetProgressBar(Config.EnochianBar, timer / 1000, 30, 0f, player); + AddDrawActions(bar.GetDrawActions(origin, Config.EnochianBar.StrataLevel)); + } + + protected unsafe void DrawPolyglotBar(Vector2 origin, IPlayerCharacter player) + { + BLMGauge _gauge = Plugin.JobGauges.Get(); + BlackMageGaugeTmp* gauge = (BlackMageGaugeTmp*)_gauge.Address; + + if (Config.PolyglotBar.HideWhenInactive && gauge->PolyglotStacks == 0) + { + return; + } + + // only 1 stack before level 80 + if (player.Level < 80) + { + BarGlowConfig? glow = gauge->PolyglotStacks == 1 && Config.PolyglotBar.GlowConfig.Enabled ? Config.PolyglotBar.GlowConfig : null; + BarHud bar = BarUtilities.GetBar(Config.PolyglotBar, gauge->PolyglotStacks, 1, 0, glowConfig: glow); + AddDrawActions(bar.GetDrawActions(origin, Config.PolyglotBar.StrataLevel)); + } + // 2-3 stacks after + else + { + int stacks = player.Level < 98 ? 2 : 3; + BarGlowConfig? glow = Config.PolyglotBar.GlowConfig.Enabled ? Config.PolyglotBar.GlowConfig : null; + BarHud[] bars = BarUtilities.GetChunkedBars(Config.PolyglotBar, stacks, gauge->PolyglotStacks, stacks, 0, glowConfig: glow); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.PolyglotBar.StrataLevel)); + } + } + } + + protected unsafe void DrawParadoxBar(Vector2 origin, IPlayerCharacter player) + { + BLMGauge _gauge = Plugin.JobGauges.Get(); + BlackMageGaugeTmp* gauge = (BlackMageGaugeTmp*)_gauge.Address; + bool inUmbralIce = gauge->ElementStance < 0; + bool inAstralFire = gauge->ElementStance > 0; + + if (Config.ParadoxBar.HideWhenInactive && !gauge->ParadoxActive) + { + return; + }; + + PluginConfigColor color = Config.ParadoxBar.FillColor; + if (Config.ParadoxBar.UseElementColor) + { + color = inUmbralIce ? Config.ParadoxBar.IceColor : (inAstralFire ? Config.ParadoxBar.FireColor : color); + } + + BarGlowConfig? glow = gauge->ParadoxActive && Config.ParadoxBar.GlowConfig.Enabled ? Config.ParadoxBar.GlowConfig : null; + BarHud bar = BarUtilities.GetBar(Config.ParadoxBar, gauge->ParadoxActive ? 1 : 0, 1, 0, fillColor: color, glowConfig: glow); + AddDrawActions(bar.GetDrawActions(origin, Config.ParadoxBar.StrataLevel)); + } + + protected void DrawThunderDoTBar(Vector2 origin, IPlayerCharacter player) + { + IGameObject? target = Plugin.TargetManager.SoftTarget ?? Plugin.TargetManager.Target; + + BarHud? bar = BarUtilities.GetDoTBar(Config.ThunderDoTBar, player, target, ThunderDoTIDs, ThunderDoTDurations); + if (bar != null) + { + AddDrawActions(bar.GetDrawActions(origin, Config.ThunderDoTBar.StrataLevel)); + } + } + } + + [Section("Job Specific Bars")] + [SubSection("Caster", 0)] + [SubSection("Black Mage", 1)] + public class BlackMageConfig : JobConfig + { + [JsonIgnore] public override uint JobId => JobIDs.BLM; + + public new static BlackMageConfig DefaultConfig() + { + var config = new BlackMageConfig(); + + config.EnochianBar.Label.FontID = FontsConfig.DefaultMediumFontKey; + config.EnochianBar.Label.TextAnchor = DrawAnchor.Left; + config.EnochianBar.Label.FrameAnchor = DrawAnchor.Left; + config.EnochianBar.Label.Position = new Vector2(2, 0); + + config.ThunderDoTBar.Label.FontID = FontsConfig.DefaultMediumFontKey; + config.ThunderDoTBar.Label.TextAnchor = DrawAnchor.Left; + config.ThunderDoTBar.Label.FrameAnchor = DrawAnchor.Left; + config.ThunderDoTBar.Label.Position = new Vector2(2, 0); + + return config; + } + + [NestedConfig("Mana Bar", 30)] + public BlackMageManaBarConfig ManaBar = new BlackMageManaBarConfig( + new Vector2(0, -10), + new Vector2(254, 18), + new PluginConfigColor(new Vector4(234f / 255f, 95f / 255f, 155f / 255f, 100f / 100f)) + ); + + [NestedConfig("Umbral Ice / Astral Fire Bar", 31)] + public BlackMageStacksBarConfig StacksBar = new BlackMageStacksBarConfig( + new(-67, -27), + new(120, 10) + ); + + [NestedConfig("Umbral Heart Bar", 32)] + public ChunkedBarConfig UmbralHeartBar = new ChunkedBarConfig( + new(67, -27), + new(120, 10), + new PluginConfigColor(new Vector4(125f / 255f, 195f / 255f, 205f / 255f, 100f / 100f)) + ); + + [NestedConfig("Paradox Bar", 33)] + public BlackMageParadoxBarConfig ParadoxBar = new BlackMageParadoxBarConfig( + new(0, -27), + new(10, 10), + new PluginConfigColor(new Vector4(123f / 255f, 66f / 255f, 177f / 255f, 100f / 100f)) + ); + + [NestedConfig("Enochian Bar", 40)] + public ProgressBarConfig EnochianBar = new ProgressBarConfig( + new(-16, -41), + new(222, 14), + new PluginConfigColor(new Vector4(234f / 255f, 95f / 255f, 155f / 255f, 100f / 100f)) + ); + + [NestedConfig("Polyglot Bar", 45)] + public BlackMagePolyglotBarConfig PolyglotBar = new BlackMagePolyglotBarConfig( + new(112, -41), + new(30, 14), + new PluginConfigColor(new Vector4(234f / 255f, 95f / 255f, 155f / 255f, 100f / 100f)) + ); + + [NestedConfig("Astral Soul Bar", 45)] + public BlackMageAstralSoulBarConfig AstralSoulBar = new BlackMageAstralSoulBarConfig( + new(112, -41), + new(30, 14), + new PluginConfigColor(new Vector4(220f / 255f, 180f / 255f, 180f / 255f, 100f / 100f)) + ); + + [NestedConfig("Triplecast Bar", 50)] + public BlackMageTriplecastBarConfig TriplecastBar = new BlackMageTriplecastBarConfig( + new(0, -55), + new(254, 10), + new PluginConfigColor(new Vector4(255f / 255f, 255f / 255f, 255f / 255f, 100f / 100f)) + ); + + [NestedConfig("Thunder DoT Bar", 60)] + public ProgressBarConfig ThunderDoTBar = new ProgressBarConfig( + new(64, -69), + new(126, 14), + new PluginConfigColor(new Vector4(67f / 255f, 187 / 255f, 255f / 255f, 100f / 100f)) + ); + } + + [Exportable(false)] + public class BlackMageManaBarConfig : BarConfig + { + [Checkbox("Use Element Color" + "##MP", spacing = true)] + [Order(50)] + public bool UseElementColor = true; + + [ColorEdit4("Ice Color" + "##MP")] + [Order(51, collapseWith = nameof(UseElementColor))] + public PluginConfigColor IceColor = new PluginConfigColor(new Vector4(69f / 255f, 115f / 255f, 202f / 255f, 100f / 100f)); + + [ColorEdit4("Ice Background Color" + "##MP")] + [Order(52, collapseWith = nameof(UseElementColor))] + public PluginConfigColor IceBackgroundColor = new PluginConfigColor(new Vector4(50f / 255f, 80f / 255f, 130f / 255f, 50f / 100f)); + + [ColorEdit4("Fire Color" + "##MP")] + [Order(53, collapseWith = nameof(UseElementColor))] + public PluginConfigColor FireColor = new PluginConfigColor(new Vector4(204f / 255f, 40f / 255f, 40f / 255f, 100f / 100f)); + + [ColorEdit4("Fire Background Color" + "##MP")] + [Order(54, collapseWith = nameof(UseElementColor))] + public PluginConfigColor FireBackgroundColor = new PluginConfigColor(new Vector4(120f / 255f, 30f / 255f, 30f / 255f, 50f / 100f)); + + [NestedConfig("Value Label", 60, separator = false, spacing = true)] + public NumericLabelConfig ValueLabelConfig = new NumericLabelConfig(new Vector2(2, 0), "", DrawAnchor.Left, DrawAnchor.Left); + + [NestedConfig("Threshold", 70, separator = false, spacing = true)] + public BlackMakeManaBarThresholdConfig ThresholdConfig = new BlackMakeManaBarThresholdConfig(); + + public BlackMageManaBarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) + : base(position, size, fillColor) + { + } + } + + [Exportable(false)] + public class BlackMakeManaBarThresholdConfig : ThresholdConfig + { + [Checkbox("Show Only During Astral Fire")] + [Order(5)] + public bool ShowOnlyDuringAstralFire = true; + + public BlackMakeManaBarThresholdConfig() + { + Enabled = true; + Value = 2400; + Color = new PluginConfigColor(new Vector4(240f / 255f, 120f / 255f, 10f / 255f, 100f / 100f)); + ShowMarker = true; + MarkerColor = new PluginConfigColor(new Vector4(255f / 255f, 255f / 255f, 255f / 255f, 100f / 100f)); + } + } + + [DisableParentSettings("FillColor", "FillDirection")] + [Exportable(false)] + public class BlackMageStacksBarConfig : ChunkedBarConfig + { + [ColorEdit4("Ice Color" + "##MP")] + [Order(26)] + public PluginConfigColor IceColor = new PluginConfigColor(new Vector4(69f / 255f, 115f / 255f, 202f / 255f, 100f / 100f)); + + [ColorEdit4("Fire Color" + "##MP")] + [Order(27)] + public PluginConfigColor FireColor = new PluginConfigColor(new Vector4(204f / 255f, 40f / 255f, 40f / 255f, 100f / 100f)); + + public BlackMageStacksBarConfig(Vector2 position, Vector2 size) + : base(position, size, PluginConfigColor.Empty) + { + } + } + + [Exportable(false)] + public class BlackMagePolyglotBarConfig : ChunkedBarConfig + { + [NestedConfig("Show Glow", 60, separator = false, spacing = true)] + public BarGlowConfig GlowConfig = new BarGlowConfig(); + + public BlackMagePolyglotBarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) + : base(position, size, fillColor) + { + } + } + + [Exportable(false)] + public class BlackMageParadoxBarConfig : BarConfig + { + [Checkbox("Use Element Color" + "##Paradox", spacing = true)] + [Order(50)] + public bool UseElementColor = true; + + [ColorEdit4("Ice Color" + "##Paradox")] + [Order(51, collapseWith = nameof(UseElementColor))] + public PluginConfigColor IceColor = new PluginConfigColor(new Vector4(69f / 255f, 115f / 255f, 202f / 255f, 100f / 100f)); + + [ColorEdit4("Fire Color" + "##Paradox")] + [Order(52, collapseWith = nameof(UseElementColor))] + public PluginConfigColor FireColor = new PluginConfigColor(new Vector4(204f / 255f, 40f / 255f, 40f / 255f, 100f / 100f)); + + [NestedConfig("Show Glow" + "##Paradox", 60, separator = false, spacing = true)] + public BarGlowConfig GlowConfig = new BarGlowConfig(); + + public BlackMageParadoxBarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) + : base(position, size, fillColor) + { + } + } + + [Exportable(false)] + public class BlackMageTriplecastBarConfig : ChunkedBarConfig + { + [Checkbox("Count Swiftcast" + "##Triplecast", spacing = true)] + [Order(50)] + public bool CountSwiftcast = false; + + public BlackMageTriplecastBarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) + : base(position, size, fillColor) + { + } + } + + [DisableParentSettings("FillDirection")] + [Exportable(false)] + public class BlackMageAstralSoulBarConfig : ChunkedBarConfig + { + [ColorEdit4("Full Stacks Color")] + [Order(26)] + public PluginConfigColor FullStacksColor = new PluginConfigColor(new Vector4(255f / 255f, 200f / 255f, 160f / 255f, 100f / 100f)); + + [NestedConfig("Show Glow" + "##AstralSoul", 60, separator = false, spacing = true)] + public BarGlowConfig GlowConfig = new BarGlowConfig(); + + public BlackMageAstralSoulBarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) + : base(position, size, fillColor) + { + } + } +} + +[StructLayout(LayoutKind.Explicit, Size = 0x30)] +public struct BlackMageGaugeTmp +{ + [FieldOffset(0x08)] public short EnochianTimer; + [FieldOffset(0x0A)] public sbyte ElementStance; + [FieldOffset(0x0B)] public byte UmbralHearts; + [FieldOffset(0x0C)] public byte PolyglotStacks; + [FieldOffset(0x0D)] public EnochianFlags EnochianFlags; + + public int UmbralStacks => ElementStance >= 0 ? 0 : ElementStance * -1; + public int AstralStacks => ElementStance <= 0 ? 0 : ElementStance; + public bool EnochianActive => EnochianFlags.HasFlag(EnochianFlags.Enochian); + public bool ParadoxActive => EnochianFlags.HasFlag(EnochianFlags.Paradox); + public int AstralSoulStacks => ((int)EnochianFlags >> 2) & 7; +} \ No newline at end of file diff --git a/Interface/Jobs/BlueMageHud.cs b/Interface/Jobs/BlueMageHud.cs new file mode 100644 index 0000000..0b7aacd --- /dev/null +++ b/Interface/Jobs/BlueMageHud.cs @@ -0,0 +1,313 @@ +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Game.ClientState.Statuses; +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using Newtonsoft.Json; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace HSUI.Interface.Jobs +{ + public class BlueMageHud : JobHud + { + private new BlueMageConfig Config => (BlueMageConfig)_config; + + public BlueMageHud(BlueMageConfig config, string? displayName = null) : base(config, displayName) + { + } + protected override (List, List) ChildrenPositionsAndSizes() + { + List positions = new List(); + List sizes = new List(); + + if (Config.BleedBar.Enabled) + { + positions.Add(Config.Position + Config.BleedBar.Position); + sizes.Add(Config.BleedBar.Size); + } + + if (Config.WindburnBar.Enabled) + { + positions.Add(Config.Position + Config.WindburnBar.Position); + sizes.Add(Config.WindburnBar.Size); + } + + if (Config.SurpanakhaBar.Enabled) + { + positions.Add(Config.Position + Config.SurpanakhaBar.Position); + sizes.Add(Config.SurpanakhaBar.Size); + } + + if (Config.OffGuardBar.Enabled) + { + positions.Add(Config.Position + Config.OffGuardBar.Position); + sizes.Add(Config.OffGuardBar.Size); + } + + return (positions, sizes); + } + + public override void DrawJobHud(Vector2 origin, IPlayerCharacter player) + { + Vector2 pos = origin + Config.Position; + if (Config.BleedBar.Enabled) + { + DrawBleedBar(pos, player); + } + + if (Config.WindburnBar.Enabled) + { + DrawWindburnBar(pos, player); + } + + if (Config.SurpanakhaBar.Enabled) + { + DrawSurpanakhaBar(pos, player); + } + + if (Config.OffGuardBar.Enabled) + { + DrawOffGuardBar(pos, player); + } + + if (Config.MoonFluteBar.Enabled) + { + DrawMoonFluteBar(pos, player); + } + + if (Config.SpellAmpBar.Enabled) + { + DrawSpellAmpBar(pos, player); + } + + if (Config.TingleBar.Enabled) + { + DrawTingleBar(pos, player); + } + } + + private static List BleedID = new List { 1714 }; + private static List BleedDurations = new List { 30, 60 }; + + protected void DrawBleedBar(Vector2 origin, IPlayerCharacter player) + { + var target = Plugin.TargetManager.SoftTarget ?? Plugin.TargetManager.Target; + BarHud? bar = BarUtilities.GetDoTBar(Config.BleedBar, player, target, BleedID, BleedDurations); + if (bar != null) + { + AddDrawActions(bar.GetDrawActions(origin, Config.BleedBar.StrataLevel)); + } + } + + protected void DrawWindburnBar(Vector2 origin, IPlayerCharacter player) + { + IGameObject? target = Plugin.TargetManager.SoftTarget ?? Plugin.TargetManager.Target; + bool dotExists = false; + + if (target != null && target is IBattleChara targetChara) + { + dotExists = Utils.StatusListForBattleChara(targetChara).FirstOrDefault(o => o.SourceId == player.GameObjectId && o.StatusId == 1723) != null; + } + + if (dotExists) + { + BarHud? bar = BarUtilities.GetDoTBar(Config.WindburnBar, player, target, 1723, 6f); + if (bar != null) + { + AddDrawActions(bar.GetDrawActions(origin, Config.WindburnBar.StrataLevel)); + } + } + else + { + float featherRainCD = SpellHelper.Instance.GetSpellCooldown(11426); + float max = 30f; + float current = max - featherRainCD; + + if (!Config.WindburnBar.HideWhenInactive || current < max) + { + Config.WindburnBar.Label.SetValue(max - current); + if (current == max) + { + Config.WindburnBar.Label.SetText("Ready"); + } + + BarHud bar = BarUtilities.GetProgressBar(Config.WindburnBar, current, max, 0f, player); + AddDrawActions(bar.GetDrawActions(origin, Config.WindburnBar.StrataLevel)); + } + } + } + + protected void DrawSurpanakhaBar(Vector2 origin, IPlayerCharacter player) + { + float surpanakhaCD = SpellHelper.Instance.GetSpellCooldown(18323); + float max = 120f; + float current = max - surpanakhaCD; + + if (!Config.SurpanakhaBar.HideWhenInactive || current < max) + { + Config.SurpanakhaBar.Label.SetValue((max - current) % 30); + + BarHud[] bars = BarUtilities.GetChunkedProgressBars(Config.SurpanakhaBar, 4, current, max, 0f, player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.SurpanakhaBar.StrataLevel)); + } + } + } + + protected void DrawOffGuardBar(Vector2 origin, IPlayerCharacter player) + { + var target = Plugin.TargetManager.SoftTarget ?? Plugin.TargetManager.Target; + + BarHud? bar = BarUtilities.GetDoTBar(Config.OffGuardBar, player, target, 1717, 15f); + if (bar != null) + { + AddDrawActions(bar.GetDrawActions(origin, Config.OffGuardBar.StrataLevel)); + } + } + + protected void DrawMoonFluteBar(Vector2 origin, IPlayerCharacter player) + { + IStatus? buff = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 1718 or 1727 && o.RemainingTime > 0f); + if (!Config.MoonFluteBar.HideWhenInactive || buff is not null) + { + var buffColor = buff is not null ? buff.StatusId switch + { + 1718 => Config.MoonFluteBar.WaxingCrescentColor, + 1727 => Config.MoonFluteBar.WaningCrescentColor, + _ => Config.MoonFluteBar.WaxingCrescentColor + } : Config.MoonFluteBar.WaxingCrescentColor; + + float buffDuration = buff?.RemainingTime ?? 0f; + + Config.MoonFluteBar.Label.SetValue(buffDuration); + + BarHud bar = BarUtilities.GetProgressBar(Config.MoonFluteBar, buffDuration, 15f, 0, player, fillColor: buffColor); + AddDrawActions(bar.GetDrawActions(origin, Config.MoonFluteBar.StrataLevel)); + } + } + + protected void DrawSpellAmpBar(Vector2 origin, IPlayerCharacter player) + { + IStatus? buff = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 2118 or 1716 && o.RemainingTime > 0f); + if (!Config.SpellAmpBar.HideWhenInactive || buff is not null) + { + var buffColor = buff is not null ? buff.StatusId switch + { + 2118 => Config.SpellAmpBar.BristleColor, + 1716 => Config.SpellAmpBar.WhistleColor, + _ => Config.SpellAmpBar.BristleColor + } : Config.SpellAmpBar.BristleColor; + + float buffDuration = buff?.RemainingTime ?? 0f; + + Config.SpellAmpBar.Label.SetValue(buffDuration); + + BarHud bar = BarUtilities.GetProgressBar(Config.SpellAmpBar, buffDuration, 30f, 0, player, fillColor: buffColor); + AddDrawActions(bar.GetDrawActions(origin, Config.SpellAmpBar.StrataLevel)); + } + } + + protected void DrawTingleBar(Vector2 origin, IPlayerCharacter player) + { + BarHud? bar = BarUtilities.GetProcBar(Config.TingleBar, player, 2492, 15f); + if (bar != null) + { + AddDrawActions(bar.GetDrawActions(origin, Config.TingleBar.StrataLevel)); + } + } + } + + [Section("Job Specific Bars")] + [SubSection("Caster", 0)] + [SubSection("Blue Mage", 1)] + public class BlueMageConfig : JobConfig + { + [JsonIgnore] public override uint JobId => JobIDs.BLU; + public new static BlueMageConfig DefaultConfig() + { + var config = new BlueMageConfig(); + config.UseDefaultPrimaryResourceBar = true; + + return config; + } + [NestedConfig("Bleed Bar", 40)] + public ProgressBarConfig BleedBar = new ProgressBarConfig( + new(-64, -55), + new(126, 14), + new PluginConfigColor(new Vector4(106f / 255f, 237f / 255f, 241f / 255f, 100f / 100f)), + BarDirection.Left + ); + [NestedConfig("Windburn Bar", 45)] + public ProgressBarConfig WindburnBar = new ProgressBarConfig( + new(64, -55), + new(126, 14), + new PluginConfigColor(new Vector4(50f / 255f, 93f / 255f, 37f / 255f, 100f / 100f)) + ); + [NestedConfig("Surpanakha Bar", 50)] + public ChunkedProgressBarConfig SurpanakhaBar = new ChunkedProgressBarConfig( + new(0, -39), + new(254, 14), + new PluginConfigColor(new Vector4(202f / 255f, 228f / 255f, 246f / 242f, 100f / 100f)) + + ); + [NestedConfig("Off-Guard Bar", 55)] + public ProgressBarConfig OffGuardBar = new ProgressBarConfig( + new(0, -23), + new(254, 14), + new PluginConfigColor(new Vector4(202f / 255f, 228f / 255f, 246f / 242f, 100f / 100f)) + ); + + [NestedConfig("Moon Flute Bar", 60)] + public MoonFluteBarConfig MoonFluteBar = new MoonFluteBarConfig( + new(0, -7), + new(84, 14), + new(new Vector4(128f / 255f, 255f / 255f, 255f / 255f, 100f / 100f)) + ); + [NestedConfig("Spell Amp Bar", 65)] + public SpellAmpBarConfig SpellAmpBar = new SpellAmpBarConfig( + new(-86, -7), + new(82, 14), + new(new Vector4(128f / 255f, 255f / 255f, 255f / 255f, 100f / 100f)) + ); + [NestedConfig("Tingle Bar", 70)] + public ProgressBarConfig TingleBar = new ProgressBarConfig( + new(86, -7), + new(82, 14), + new(new Vector4(128f / 255f, 255f / 255f, 255f / 255f, 100f / 100f)) + ); + } + + [Exportable(false)] + public class MoonFluteBarConfig : ProgressBarConfig + { + [ColorEdit4("Waning Crescent Color")] + [Order(26)] + public PluginConfigColor WaxingCrescentColor = new(new Vector4(0f / 255f, 255f / 255f, 0f / 255f, 100f / 100f)); + [ColorEdit4("Waxing Crescent Color")] + [Order(27)] + public PluginConfigColor WaningCrescentColor = new(new Vector4(255f / 255f, 0f / 255f, 0f / 255f, 100f / 100f)); + public MoonFluteBarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) + : base(position, size, fillColor) + { + } + } + [Exportable(false)] + public class SpellAmpBarConfig : ProgressBarConfig + { + [ColorEdit4("Waning Crescent Color")] + [Order(26)] + public PluginConfigColor BristleColor = new(new Vector4(0f / 255f, 255f / 255f, 0f / 255f, 100f / 100f)); + [ColorEdit4("Waxing Crescent Color")] + [Order(27)] + public PluginConfigColor WhistleColor = new(new Vector4(255f / 255f, 0f / 255f, 0f / 255f, 100f / 100f)); + public SpellAmpBarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) + : base(position, size, fillColor) + { + } + } +} diff --git a/Interface/Jobs/CraftersConfig.cs b/Interface/Jobs/CraftersConfig.cs new file mode 100644 index 0000000..adddefc --- /dev/null +++ b/Interface/Jobs/CraftersConfig.cs @@ -0,0 +1,56 @@ +using HSUI.Helpers; +using Newtonsoft.Json; + +namespace HSUI.Interface.Jobs +{ + public class CraftersConfig : JobConfig + { + public override uint JobId => 0; + + public CraftersConfig() + { + UseDefaultPrimaryResourceBar = true; + PrimaryResourceType = PrimaryResourceTypes.CP; + } + } + + public class CarpenterConfig : CraftersConfig + { + [JsonIgnore] public override uint JobId => JobIDs.CRP; + } + + public class BlacksmithConfig : CraftersConfig + { + [JsonIgnore] public override uint JobId => JobIDs.BSM; + } + + public class ArmorerConfig : CraftersConfig + { + [JsonIgnore] public override uint JobId => JobIDs.ARM; + } + + public class GoldsmithConfig : CraftersConfig + { + [JsonIgnore] public override uint JobId => JobIDs.GSM; + } + + public class LeatherworkerConfig : CraftersConfig + { + [JsonIgnore] public override uint JobId => JobIDs.LTW; + } + + public class WeaverConfig : CraftersConfig + { + [JsonIgnore] public override uint JobId => JobIDs.WVR; + } + + public class AlchemistConfig : CraftersConfig + { + [JsonIgnore] public override uint JobId => JobIDs.ALC; + } + + public class CulinarianConfig : CraftersConfig + { + [JsonIgnore] public override uint JobId => JobIDs.CUL; + } +} diff --git a/Interface/Jobs/DancerHud.cs b/Interface/Jobs/DancerHud.cs new file mode 100644 index 0000000..3d80d68 --- /dev/null +++ b/Interface/Jobs/DancerHud.cs @@ -0,0 +1,430 @@ +using Dalamud.Game.ClientState.JobGauge.Types; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Statuses; +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using HSUI.Interface.GeneralElements; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace HSUI.Interface.Jobs +{ + public class DancerHud : JobHud + { + private new DancerConfig Config => (DancerConfig)_config; + + public DancerHud(DancerConfig config, string? displayName = null) : base(config, displayName) + { + + } + + protected override (List, List) ChildrenPositionsAndSizes() + { + List positions = new List(); + List sizes = new List(); + + if (Config.StandardFinishBar.Enabled) + { + positions.Add(Config.Position + Config.StandardFinishBar.Position); + sizes.Add(Config.StandardFinishBar.Size); + } + + if (Config.TechnicalFinishBar.Enabled) + { + positions.Add(Config.Position + Config.TechnicalFinishBar.Position); + sizes.Add(Config.TechnicalFinishBar.Position); + } + + if (Config.DevilmentBar.Enabled) + { + positions.Add(Config.Position + Config.DevilmentBar.Position); + sizes.Add(Config.DevilmentBar.Position); + } + + if (Config.EspritGauge.Enabled) + { + positions.Add(Config.Position + Config.EspritGauge.Position); + sizes.Add(Config.EspritGauge.Size); + } + + if (Config.FeatherGauge.Enabled) + { + positions.Add(Config.Position + Config.FeatherGauge.Position); + sizes.Add(Config.FeatherGauge.Size); + } + + if (Config.CascadeBar.Enabled) + { + positions.Add(Config.Position + Config.CascadeBar.Position); + sizes.Add(Config.CascadeBar.Position); + } + + if (Config.FountainBar.Enabled) + { + positions.Add(Config.Position + Config.FountainBar.Position); + sizes.Add(Config.FountainBar.Position); + } + + return (positions, sizes); + } + + public override void DrawJobHud(Vector2 origin, IPlayerCharacter player) + { + Vector2 pos = origin + Config.Position; + + if (Config.EspritGauge.Enabled) + { + DrawEspritBar(pos, player); + } + + if (Config.FeatherGauge.Enabled) + { + DrawFeathersBar(pos, player); + } + + if (Config.StandardFinishBar.Enabled) + { + DrawStandardBar(pos, player); + } + + if (Config.TechnicalFinishBar.Enabled) + { + DrawTechnicalBar(pos, player); + } + + if (Config.DevilmentBar.Enabled) + { + DrawDevilmentBar(pos, player); + } + + bool showingStepBar = false; + if (Config.StepsBar.Enabled) + { + showingStepBar = DrawStepBar(pos, player); + } + + if (!showingStepBar || !Config.StepsBar.HideProcs) + { + if (Config.CascadeBar.Enabled) { DrawProcBar(pos, player, Config.CascadeBar, 2693, 3017); } + if (Config.FountainBar.Enabled) { DrawProcBar(pos, player, Config.FountainBar, 2694, 3018); } + } + } + + private void DrawProcBar(Vector2 origin, IPlayerCharacter player, DancerProcBarConfig config, params uint[] statusIDs) + { + List durations = new List(); + for (int i = 0; i < statusIDs.Length; i++) + { + durations.Add(30f); + } + + BarHud? bar = BarUtilities.GetProcBar(config, player, statusIDs.ToList(), durations, !config.IgnoreBuffDuration); + if (bar != null) + { + AddDrawActions(bar.GetDrawActions(origin, config.StrataLevel)); + } + } + + private unsafe bool DrawStepBar(Vector2 origin, IPlayerCharacter player) + { + DNCGauge gauge = Plugin.JobGauges.Get(); + if (!gauge.IsDancing) + { + return false; + } + + List> chunks = new List>(); + List glows = new List(); + bool danceReady = true; + + for (var i = 0; i < 4; i++) + { + DNCStep step = (DNCStep)gauge.Steps[i]; + + if (step == DNCStep.None) + { + break; + } + + if (gauge.CompletedSteps == i) + { + glows.Add(true); + danceReady = false; + } + else + { + glows.Add(false); + } + + PluginConfigColor color = PluginConfigColor.Empty; + + switch (step) + { + case DNCStep.Emboite: + color = Config.StepsBar.EmboiteColor; + break; + + case DNCStep.Entrechat: + color = Config.StepsBar.EntrechatColor; + break; + + case DNCStep.Jete: + color = Config.StepsBar.JeteColor; + break; + + case DNCStep.Pirouette: + color = Config.StepsBar.PirouetteColor; + break; + } + + var tuple = new Tuple(color, 1, null); + chunks.Add(tuple); + } + + if (danceReady) + { + for (int i = 0; i < glows.Count; i++) + { + glows[i] = true; + } + } + + BarHud[] bars = BarUtilities.GetChunkedBars(Config.StepsBar, chunks.ToArray(), player, Config.StepsBar.GlowConfig, glows.ToArray()); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.StepsBar.StrataLevel)); + } + + return true; + } + + private void DrawEspritBar(Vector2 origin, IPlayerCharacter player) + { + DNCGauge gauge = Plugin.JobGauges.Get(); + + if (Config.EspritGauge.HideWhenInactive && gauge.Esprit is 0) { return; } + + Config.EspritGauge.Label.SetValue(gauge.Esprit); + + BarHud[] bars = BarUtilities.GetChunkedProgressBars(Config.EspritGauge, 2, gauge.Esprit, 100, 0f, player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.EspritGauge.StrataLevel)); + } + } + + private void DrawFeathersBar(Vector2 origin, IPlayerCharacter player) + { + DNCGauge gauge = Plugin.JobGauges.Get(); + bool hasFlourishingBuff = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 1820 or 2021) != null; + bool[]? glows = null; + + if (Config.FeatherGauge.HideWhenInactive && gauge.Feathers is 0 && !hasFlourishingBuff) + { + return; + } + + if (Config.FeatherGauge.GlowConfig.Enabled) + { + glows = new bool[] { hasFlourishingBuff, hasFlourishingBuff, hasFlourishingBuff, hasFlourishingBuff }; + } + + BarGlowConfig? config = hasFlourishingBuff ? Config.FeatherGauge.GlowConfig : null; + BarHud[] bars = BarUtilities.GetChunkedBars(Config.FeatherGauge, 4, gauge.Feathers, 4, 0, player, glowConfig: config, chunksToGlow: glows); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.FeatherGauge.StrataLevel)); + } + } + + private void DrawTechnicalBar(Vector2 origin, IPlayerCharacter player) + { + IEnumerable devilmentBuff = Utils.StatusListForBattleChara(player).Where(o => o.StatusId is 1825 && o.SourceId == player.GameObjectId); + + float technicalFinishDuration = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 1822 or 2050 && o.SourceId == player.GameObjectId)?.RemainingTime ?? 0f; + + if (!Config.TechnicalFinishBar.HideWhenInactive || technicalFinishDuration > 0) + { + Config.TechnicalFinishBar.Label.SetValue(Math.Abs(technicalFinishDuration)); + + BarHud bar = BarUtilities.GetProgressBar(Config.TechnicalFinishBar, technicalFinishDuration, 20f, 0f, player); + AddDrawActions(bar.GetDrawActions(origin, Config.TechnicalFinishBar.StrataLevel)); + } + } + + private void DrawDevilmentBar(Vector2 origin, IPlayerCharacter player) + { + float devilmentDuration = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 1825 && o.SourceId == player.GameObjectId)?.RemainingTime ?? 0f; + + if (!Config.DevilmentBar.HideWhenInactive || devilmentDuration > 0) + { + Config.DevilmentBar.Label.SetValue(Math.Abs(devilmentDuration)); + + BarHud bar = BarUtilities.GetProgressBar(Config.DevilmentBar, devilmentDuration, 20f, 0f, player); + AddDrawActions(bar.GetDrawActions(origin, Config.DevilmentBar.StrataLevel)); + } + } + + private void DrawStandardBar(Vector2 origin, IPlayerCharacter player) + { + float standardFinishDuration = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 1821 or 2024 or 2105 or 2113 && o.SourceId == player.GameObjectId)?.RemainingTime ?? 0f; + + if (!Config.StandardFinishBar.HideWhenInactive || standardFinishDuration > 0) + { + Config.StandardFinishBar.Label.SetValue(Math.Abs(standardFinishDuration)); + + BarHud bar = BarUtilities.GetProgressBar(Config.StandardFinishBar, standardFinishDuration, 60f, 0f, player); + AddDrawActions(bar.GetDrawActions(origin, Config.StandardFinishBar.StrataLevel)); + } + } + } + + public enum DNCStep : uint + { + None = 15998, + Emboite = 15999, + Entrechat = 16000, + Jete = 16001, + Pirouette = 16002 + } + + [Section("Job Specific Bars")] + [SubSection("Ranged", 0)] + [SubSection("Dancer", 1)] + public class DancerConfig : JobConfig + { + [JsonIgnore] public override uint JobId => JobIDs.DNC; + public new static DancerConfig DefaultConfig() + { + var config = new DancerConfig(); + + config.EspritGauge.UseChunks = false; + config.EspritGauge.Label.Enabled = true; + + config.CascadeBar.Label.FontID = FontsConfig.DefaultMediumFontKey; + config.FountainBar.Label.FontID = FontsConfig.DefaultMediumFontKey; + + return config; + } + + + [NestedConfig("Standard Finish Bar", 30)] + public ProgressBarConfig StandardFinishBar = new ProgressBarConfig( + new(0, -10), + new(254, 20), + new PluginConfigColor(new Vector4(0f / 255f, 193f / 255f, 95f / 255f, 100f / 100f)) + ); + + [NestedConfig("Technical Finish Bar", 35)] + public ProgressBarConfig TechnicalFinishBar = new ProgressBarConfig( + new(-64, -32), + new(126, 20), + new PluginConfigColor(new Vector4(255f / 255f, 9f / 255f, 102f / 255f, 100f / 100f)) + ); + + [NestedConfig("Devilment Bar", 40)] + public ProgressBarConfig DevilmentBar = new ProgressBarConfig( + new(64, -32), + new(126, 20), + new PluginConfigColor(new Vector4(52f / 255f, 78f / 255f, 29f / 255f, 100f / 100f)) + ); + + [NestedConfig("Esprit Gauge", 45)] + public ChunkedProgressBarConfig EspritGauge = new ChunkedProgressBarConfig( + new(0, -54), + new(254, 20), + new PluginConfigColor(new Vector4(72f / 255f, 20f / 255f, 99f / 255f, 100f / 100f)) + ); + + [NestedConfig("Feathers Gauge", 50)] + public DancerFeatherGaugeConfig FeatherGauge = new DancerFeatherGaugeConfig( + new(0, -71), + new(254, 10), + new PluginConfigColor(new Vector4(175f / 255f, 229f / 255f, 29f / 255f, 100f / 100f)) + ); + + [NestedConfig("Flourishing Symmetry Bar", 60)] + public DancerProcBarConfig CascadeBar = new DancerProcBarConfig( + new(-96, -83), + new(62, 10), + new(new Vector4(0f / 255f, 255f / 255f, 0f / 255f, 100f / 100f)) + ); + + [NestedConfig("Flourishing Flow Bar", 65)] + public DancerProcBarConfig FountainBar = new DancerProcBarConfig( + new(-32, -83), + new(62, 10), + new(new Vector4(255f / 255f, 215f / 255f, 0f / 255f, 100f / 100f)) + ); + + [NestedConfig("Steps Bar", 80)] + public DancerStepsBarConfig StepsBar = new DancerStepsBarConfig( + new(0, -83), + new(254, 10) + ); + } + + [Exportable(false)] + public class DancerFeatherGaugeConfig : ChunkedBarConfig + { + [NestedConfig("Glow on Flourishing Fan Dance", 1000, separator = false, spacing = true)] + public BarGlowConfig GlowConfig = new BarGlowConfig(); + + public DancerFeatherGaugeConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) + : base(position, size, fillColor) + { + GlowConfig.Color = new PluginConfigColor(new Vector4(255f / 255f, 215f / 255f, 0f / 255f, 100f / 100f)); + } + } + + [Exportable(false)] + public class DancerProcBarConfig : ProgressBarConfig + { + [Checkbox("Ignore Buff Duration")] + [Order(4)] + public bool IgnoreBuffDuration = true; + + public DancerProcBarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) + : base(position, size, fillColor) + { + } + } + + [DisableParentSettings("FillColor", "HideWhenInactive")] + [Exportable(false)] + public class DancerStepsBarConfig : ChunkedBarConfig + { + [Checkbox("Hide Procs When Active")] + [Order(50)] + public bool HideProcs = true; + + [ColorEdit4("Emboite", spacing = true)] + [Order(55)] + public PluginConfigColor EmboiteColor = new(new Vector4(255f / 255f, 0f / 255f, 0f / 255f, 100f / 100f)); + + [ColorEdit4("Entrechat")] + [Order(60)] + public PluginConfigColor EntrechatColor = new(new Vector4(0f / 255f, 0f / 255f, 255f / 255f, 100f / 100f)); + + [ColorEdit4("Jete")] + [Order(65)] + public PluginConfigColor JeteColor = new(new Vector4(0f / 255f, 255f / 255f, 0f / 255f, 100f / 100f)); + + [ColorEdit4("Pirouette")] + [Order(70)] + public PluginConfigColor PirouetteColor = new(new Vector4(255f / 255f, 215f / 255f, 0f / 255f, 100f / 100f)); + + [NestedConfig("Glow", 75, separator = false, spacing = true)] + public BarGlowConfig GlowConfig = new BarGlowConfig(); + + public DancerStepsBarConfig(Vector2 position, Vector2 size) + : base(position, size, PluginConfigColor.Empty) + { + } + } +} \ No newline at end of file diff --git a/Interface/Jobs/DarkKnightHud.cs b/Interface/Jobs/DarkKnightHud.cs new file mode 100644 index 0000000..5434015 --- /dev/null +++ b/Interface/Jobs/DarkKnightHud.cs @@ -0,0 +1,344 @@ +using Dalamud.Game.ClientState.JobGauge.Types; +using Dalamud.Game.ClientState.Objects.SubKinds; +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using HSUI.Interface.GeneralElements; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Dalamud.Game.ClientState.Statuses; + +namespace HSUI.Interface.Jobs +{ + public class DarkKnightHud : JobHud + { + private new DarkKnightConfig Config => (DarkKnightConfig)_config; + + public DarkKnightHud(DarkKnightConfig config, string? displayName = null) : base(config, displayName) + { + } + + protected override (List, List) ChildrenPositionsAndSizes() + { + List positions = new(); + List sizes = new(); + + if (Config.ManaBar.Enabled) + { + positions.Add(Config.Position + Config.ManaBar.Position); + sizes.Add(Config.ManaBar.Size); + } + + if (Config.BloodGauge.Enabled) + { + positions.Add(Config.Position + Config.BloodGauge.Position); + sizes.Add(Config.BloodGauge.Size); + } + + if (Config.DarksideBar.Enabled) + { + positions.Add(Config.Position + Config.DarksideBar.Position); + sizes.Add(Config.DarksideBar.Size); + } + + if (Config.BloodWeaponBar.Enabled) + { + positions.Add(Config.Position + Config.BloodWeaponBar.Position); + sizes.Add(Config.BloodWeaponBar.Size); + } + + if (Config.DeliriumBar.Enabled) + { + positions.Add(Config.Position + Config.DeliriumBar.Position); + sizes.Add(Config.DeliriumBar.Size); + } + + if (Config.LivingShadowBar.Enabled) + { + positions.Add(Config.Position + Config.LivingShadowBar.Position); + sizes.Add(Config.LivingShadowBar.Size); + } + + return (positions, sizes); + } + + public override void DrawJobHud(Vector2 origin, IPlayerCharacter player) + { + Vector2 pos = origin + Config.Position; + + if (Config.ManaBar.Enabled) + { + DrawManaBar(pos, player); + } + + if (Config.BloodGauge.Enabled) + { + DrawBloodGauge(pos, player); + } + + if (Config.DarksideBar.Enabled) + { + DrawDarkside(pos, player); + } + + if (Config.BloodWeaponBar.Enabled) + { + DrawBloodWeaponBar(pos, player); + } + + if (Config.DeliriumBar.Enabled) + { + DrawDeliriumBar(pos, player); + } + + if (Config.LivingShadowBar.Enabled) + { + DrawLivingShadowBar(pos, player); + } + } + + private unsafe void DrawManaBar(Vector2 origin, IPlayerCharacter player) + { + DRKGauge gauge = Plugin.JobGauges.Get(); + + if (Config.ManaBar.HideWhenInactive && !gauge.HasDarkArts && player.CurrentMp == player.MaxMp) + { + return; + } + + Config.ManaBar.UsePartialFillColor = !gauge.HasDarkArts; + + Config.ManaBar.Label.SetValue(player.CurrentMp); + + // hardcoded 9k as maxMP so the chunks are each 3k since that's what a DRK wants to see + BarHud[] bars = BarUtilities.GetChunkedProgressBars( + Config.ManaBar, + gauge.HasDarkArts ? 1 : 3, + player.CurrentMp, + Config.ManaBar.ShowFullMana ? player.MaxMp : 9000, + 0f, + player, + null, + gauge.HasDarkArts ? Config.ManaBar.DarkArtsColor : Config.ManaBar.FillColor + ); + + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.ManaBar.StrataLevel)); + } + } + + private void DrawDarkside(Vector2 origin, IPlayerCharacter player) + { + DRKGauge gauge = Plugin.JobGauges.Get(); + if (Config.DarksideBar.HideWhenInactive && gauge.DarksideTimeRemaining == 0) + { + return; + }; + + float timer = Math.Abs(gauge.DarksideTimeRemaining) / 1000; + + Config.DarksideBar.Label.SetValue(timer); + + BarHud bar = BarUtilities.GetProgressBar(Config.DarksideBar, timer, 60, 0, player); + AddDrawActions(bar.GetDrawActions(origin, Config.DarksideBar.StrataLevel)); + } + + private void DrawBloodGauge(Vector2 origin, IPlayerCharacter player) + { + DRKGauge gauge = Plugin.JobGauges.Get(); + if (Config.BloodGauge.HideWhenInactive && gauge.Blood <= 0) + { + return; + } + + Config.BloodGauge.Label.SetValue(gauge.Blood); + + BarHud[] bars = BarUtilities.GetChunkedProgressBars(Config.BloodGauge, 2, gauge.Blood, 100, 0, player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.BloodGauge.StrataLevel)); + } + } + + private void DrawBloodWeaponBar(Vector2 origin, IPlayerCharacter player) + { + IStatus? bloodWeaponBuff = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 742); + float duration = bloodWeaponBuff?.RemainingTime ?? 0f; + int stacks = bloodWeaponBuff?.Param ?? 0; + + if (Config.BloodWeaponBar.HideWhenInactive && duration <= 0) + { + return; + } + + var chunks = new Tuple[3]; + + for (int i = 0; i < 3; i++) + { + chunks[i] = new(Config.BloodWeaponBar.FillColor, i < stacks ? 1 : 0, i == 1 ? Config.BloodWeaponBar.Label : null); + } + + if(Config.BloodWeaponBar.FillDirection is BarDirection.Left or BarDirection.Up) + { + chunks = chunks.Reverse().ToArray(); + } + + Config.BloodWeaponBar.Label.SetValue(duration); + + BarHud[] bars = BarUtilities.GetChunkedBars(Config.BloodWeaponBar, chunks, player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.BloodWeaponBar.StrataLevel)); + } + } + + private void DrawDeliriumBar(Vector2 origin, IPlayerCharacter player) + { + IStatus? deliriumBuff = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 1972); + float deliriumDuration = Math.Max(0f, deliriumBuff?.RemainingTime ?? 0f); + int stacks = deliriumBuff?.Param ?? 0; + + if (Config.DeliriumBar.HideWhenInactive && deliriumDuration <= 0) + { + return; + } + + var chunks = new Tuple[3]; + for (int i = 0; i < 3; i++) + { + chunks[i] = new(Config.DeliriumBar.FillColor, i < stacks ? 1 : 0, i == 1 ? Config.DeliriumBar.Label : null); + } + + if(Config.DeliriumBar.FillDirection is BarDirection.Left or BarDirection.Up) + { + chunks = chunks.Reverse().ToArray(); + } + + Config.DeliriumBar.Label.SetValue(deliriumDuration); + + BarHud[] bars = BarUtilities.GetChunkedBars(Config.DeliriumBar, chunks, player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.DeliriumBar.StrataLevel)); + } + } + + private unsafe void DrawLivingShadowBar(Vector2 origin, IPlayerCharacter player) + { + DRKGauge gauge = Plugin.JobGauges.Get(); + + if (Config.LivingShadowBar.HideWhenInactive && gauge.ShadowTimeRemaining <= 0) + { + return; + } + + float timer = Math.Abs(gauge.ShadowTimeRemaining) / 1000; + Config.LivingShadowBar.Label.SetValue(timer); + + BarHud bar = BarUtilities.GetProgressBar(Config.LivingShadowBar, timer, 20, 0, player); + AddDrawActions(bar.GetDrawActions(origin, Config.LivingShadowBar.StrataLevel)); + } + } + + [Section("Job Specific Bars")] + [SubSection("Tank", 0)] + [SubSection("Dark Knight", 1)] + public class DarkKnightConfig : JobConfig + { + [JsonIgnore] public override uint JobId => JobIDs.DRK; + public new static DarkKnightConfig DefaultConfig() + { + var config = new DarkKnightConfig(); + + config.ManaBar.Label.FontID = FontsConfig.DefaultMediumFontKey; + config.ManaBar.UsePartialFillColor = true; + + config.DarksideBar.Label.FontID = FontsConfig.DefaultMediumFontKey; + config.DarksideBar.ThresholdConfig.Enabled = true; + + config.BloodGauge.Label.FontID = FontsConfig.DefaultMediumFontKey; + config.BloodGauge.UsePartialFillColor = true; + + return config; + } + + [NestedConfig("Mana Bar", 30)] + public DarkKnightManaBarConfig ManaBar = new DarkKnightManaBarConfig( + new Vector2(0, -61), + new Vector2(254, 10), + new PluginConfigColor(new Vector4(0f / 255f, 162f / 255f, 252f / 255f, 100f / 100f)) + ); + + [NestedConfig("Blood Gauge", 35)] + public ChunkedProgressBarConfig BloodGauge = new ChunkedProgressBarConfig( + new Vector2(0, -49), + new Vector2(254, 10), + new PluginConfigColor(new Vector4(216f / 255f, 0f / 255f, 73f / 255f, 100f / 100f)), + 2, + new PluginConfigColor(new Vector4(180f / 255f, 180f / 255f, 180f / 255f, 100f / 100f)) + ); + + [NestedConfig("Darkside Bar", 40)] + public ProgressBarConfig DarksideBar = new ProgressBarConfig( + new Vector2(0, -73), + new Vector2(254, 10), + new PluginConfigColor(new Vector4(209f / 255f, 38f / 255f, 73f / 204f, 100f / 100f)), + BarDirection.Right, + new PluginConfigColor(new Vector4(160f / 255f, 0f / 255f, 0f / 255f, 100f / 100f)), + 5 + ); + + [NestedConfig("Blood Weapon Bar", 45)] + public DarkKnightChunkedBarConfig BloodWeaponBar = new DarkKnightChunkedBarConfig( + new Vector2(-64, -32), + new Vector2(126, 20), + new PluginConfigColor(new Vector4(160f / 255f, 0f / 255f, 0f / 255f, 100f / 100f)) + ); + + [NestedConfig("Delirium Bar", 50)] + public DarkKnightChunkedBarConfig DeliriumBar = new DarkKnightChunkedBarConfig( + new Vector2(64, -32), + new Vector2(126, 20), + new PluginConfigColor(new Vector4(255f / 255f, 255f / 255f, 255f / 255f, 100f / 100f)) + ); + + [NestedConfig("Living Shadow Bar", 55)] + public ProgressBarConfig LivingShadowBar = new ProgressBarConfig( + new Vector2(0, -10), + new Vector2(254, 20), + new PluginConfigColor(new Vector4(255f / 255f, 105f / 255f, 205f / 255f, 100f / 100f)) + ); + } + + [Exportable(false)] + public class DarkKnightManaBarConfig : ChunkedProgressBarConfig + { + [ColorEdit4("Dark Arts Color" + "##MP")] + [Order(26)] + public PluginConfigColor DarkArtsColor = new PluginConfigColor(new Vector4(210f / 255f, 33f / 255f, 33f / 255f, 100f / 100f)); + + [Checkbox("Show mana values up to 10k (will break thresholds)", spacing = true)] + [Order(27)] + public bool ShowFullMana = false; + + public DarkKnightManaBarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) + : base(position, size, fillColor) + { + } + } + + [Exportable(false)] + [DisableParentSettings("UseChunks", "LabelMode")] + public class DarkKnightChunkedBarConfig : ChunkedProgressBarConfig + { + public DarkKnightChunkedBarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) + : base(position, size, fillColor) + { + } + } +} diff --git a/Interface/Jobs/DragoonHud.cs b/Interface/Jobs/DragoonHud.cs new file mode 100644 index 0000000..446f20e --- /dev/null +++ b/Interface/Jobs/DragoonHud.cs @@ -0,0 +1,172 @@ +using Dalamud.Game.ClientState.JobGauge.Types; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Objects.Types; +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace HSUI.Interface.Jobs +{ + public class DragoonHud : JobHud + { + private new DragoonConfig Config => (DragoonConfig)_config; + + private static readonly List ChaosThrustIDs = new() { 118, 1312, 2719 }; + private static readonly List ChaosThrustDurations = new() { 24, 24, 24 }; + + public DragoonHud(DragoonConfig config, string? displayName = null) : base(config, displayName) + { + } + + protected override (List, List) ChildrenPositionsAndSizes() + { + List positions = new List(); + List sizes = new List(); + + if (Config.ChaosThrustBar.Enabled) + { + positions.Add(Config.Position + Config.ChaosThrustBar.Position); + sizes.Add(Config.ChaosThrustBar.Size); + } + + if (Config.PowerSurgeBar.Enabled) + { + positions.Add(Config.Position + Config.PowerSurgeBar.Position); + sizes.Add(Config.PowerSurgeBar.Size); + } + + + if (Config.FirstmindsFocusBar.Enabled) + { + positions.Add(Config.Position + Config.FirstmindsFocusBar.Position); + sizes.Add(Config.FirstmindsFocusBar.Size); + } + + if (Config.LifeOfTheDragonBar.Enabled) + { + positions.Add(Config.Position + Config.LifeOfTheDragonBar.Position); + sizes.Add(Config.LifeOfTheDragonBar.Size); + } + + return (positions, sizes); + } + + public override void DrawJobHud(Vector2 origin, IPlayerCharacter player) + { + var position = origin + Config.Position; + if (Config.ChaosThrustBar.Enabled) + { + DrawChaosThrustBar(position, player); + } + + if (Config.PowerSurgeBar.Enabled) + { + DrawPowerSurgeBar(position, player); + } + + if (Config.FirstmindsFocusBar.Enabled) + { + DrawFirstmindsFocusBars(position, player); + } + + if (Config.LifeOfTheDragonBar.Enabled) + { + DrawBloodOfTheDragonBar(position, player); + } + } + + private void DrawChaosThrustBar(Vector2 origin, IPlayerCharacter player) + { + IGameObject? target = Plugin.TargetManager.SoftTarget ?? Plugin.TargetManager.Target; + + BarHud? bar = BarUtilities.GetDoTBar(Config.ChaosThrustBar, player, target, ChaosThrustIDs, ChaosThrustDurations); + if (bar != null) + { + AddDrawActions(bar.GetDrawActions(origin, Config.ChaosThrustBar.StrataLevel)); + } + } + + private void DrawFirstmindsFocusBars(Vector2 origin, IPlayerCharacter player) + { + DRGGauge gauge = Plugin.JobGauges.Get(); + + if (!Config.FirstmindsFocusBar.HideWhenInactive || gauge.FirstmindsFocusCount > 0) + { + BarHud[] bars = BarUtilities.GetChunkedBars(Config.FirstmindsFocusBar, 2, gauge.FirstmindsFocusCount, 2, 0, player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.FirstmindsFocusBar.StrataLevel)); + } + } + } + + private void DrawBloodOfTheDragonBar(Vector2 origin, IPlayerCharacter player) + { + DRGGauge gauge = Plugin.JobGauges.Get(); + float duration = gauge.LOTDTimer / 1000f; + + if (!Config.LifeOfTheDragonBar.HideWhenInactive || duration > 0f) + { + Config.LifeOfTheDragonBar.Label.SetValue(duration); + + BarHud bar = BarUtilities.GetProgressBar(Config.LifeOfTheDragonBar, duration, 20, 0f, player); + AddDrawActions(bar.GetDrawActions(origin, Config.LifeOfTheDragonBar.StrataLevel)); + } + } + + private void DrawPowerSurgeBar(Vector2 origin, IPlayerCharacter player) + { + var duration = Math.Abs(Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 2720)?.RemainingTime ?? 0f); + if (!Config.PowerSurgeBar.HideWhenInactive || duration > 0f) + { + Config.PowerSurgeBar.Label.SetValue(duration); + + BarHud bar = BarUtilities.GetProgressBar(Config.PowerSurgeBar, duration, 30f, 0f, player); + AddDrawActions(bar.GetDrawActions(origin, Config.PowerSurgeBar.StrataLevel)); + } + } + } + + [Section("Job Specific Bars")] + [SubSection("Melee", 0)] + [SubSection("Dragoon", 1)] + public class DragoonConfig : JobConfig + { + [JsonIgnore] public override uint JobId => JobIDs.DRG; + public new static DragoonConfig DefaultConfig() { return new DragoonConfig(); } + + [NestedConfig("Chaos Thrust", 30)] + public ProgressBarConfig ChaosThrustBar = new ProgressBarConfig( + new(0, -76), + new(254, 20), + new(new Vector4(217f / 255f, 145f / 255f, 232f / 255f, 100f / 100f)) + ); + + [NestedConfig("Power Surge", 35)] + public ProgressBarConfig PowerSurgeBar = new ProgressBarConfig( + new(0, -54), + new(254, 20), + new(new Vector4(244f / 255f, 206f / 255f, 191f / 255f, 100f / 100f)) + ); + + [NestedConfig("Firstminds' Focus", 40)] + public ChunkedBarConfig FirstmindsFocusBar = new ChunkedBarConfig( + new(64, -32), + new(126, 20), + new PluginConfigColor(new(134f / 255f, 120f / 255f, 255f / 255f, 100f / 100f)) + ); + + [NestedConfig("Life of the Dragon", 45)] + public ProgressBarConfig LifeOfTheDragonBar = new ProgressBarConfig( + new(0, -10), + new(254, 20), + new(new Vector4(185f / 255f, 0f / 255f, 25f / 255f, 100f / 100f)) + ); + } +} diff --git a/Interface/Jobs/GatherersConfig.cs b/Interface/Jobs/GatherersConfig.cs new file mode 100644 index 0000000..2b45946 --- /dev/null +++ b/Interface/Jobs/GatherersConfig.cs @@ -0,0 +1,31 @@ +using HSUI.Helpers; +using Newtonsoft.Json; + +namespace HSUI.Interface.Jobs +{ + public class GatherersConfig : JobConfig + { + public override uint JobId => 0; + + public GatherersConfig() + { + UseDefaultPrimaryResourceBar = true; + PrimaryResourceType = PrimaryResourceTypes.GP; + } + } + + public class MinerConfig : GatherersConfig + { + [JsonIgnore] public override uint JobId => JobIDs.MIN; + } + + public class BotanistConfig : GatherersConfig + { + [JsonIgnore] public override uint JobId => JobIDs.BOT; + } + + public class FisherConfig : GatherersConfig + { + [JsonIgnore] public override uint JobId => JobIDs.FSH; + } +} diff --git a/Interface/Jobs/GunbreakerHud.cs b/Interface/Jobs/GunbreakerHud.cs new file mode 100644 index 0000000..85ae243 --- /dev/null +++ b/Interface/Jobs/GunbreakerHud.cs @@ -0,0 +1,129 @@ +using Dalamud.Game.ClientState.JobGauge.Types; +using Dalamud.Game.ClientState.Objects.SubKinds; +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using HSUI.Interface.GeneralElements; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace HSUI.Interface.Jobs +{ + public class GunbreakerHud : JobHud + { + private new GunbreakerConfig Config => (GunbreakerConfig)_config; + + public GunbreakerHud(GunbreakerConfig config, string? displayName = null) : base(config, displayName) { } + + protected override (List, List) ChildrenPositionsAndSizes() + { + List positions = new List(); + List sizes = new List(); + + if (Config.PowderGauge.Enabled) + { + positions.Add(Config.Position + Config.PowderGauge.Position); + sizes.Add(Config.PowderGauge.Size); + } + + if (Config.NoMercy.Enabled) + { + positions.Add(Config.Position + Config.NoMercy.Position); + sizes.Add(Config.NoMercy.Size); + } + + return (positions, sizes); + } + + public override void DrawJobHud(Vector2 origin, IPlayerCharacter player) + { + if (Config.PowderGauge.Enabled) + { + DrawPowderGauge(origin + Config.Position, player); + } + + if (Config.NoMercy.Enabled) + { + DrawNoMercyBar(origin + Config.Position, player); + } + } + + private void DrawPowderGauge(Vector2 origin, IPlayerCharacter player) + { + GNBGauge gauge = Plugin.JobGauges.Get(); + if (Config.PowderGauge.HideWhenInactive && gauge.Ammo == 0) + { + return; + } + + PluginConfigColor mainColor = Config.PowderGauge.FillColor; + PluginConfigColor extraColor = Config.PowderGauge.BloodfestExtraCartridgesColor; + int maxCartridges = player.Level >= 88 ? 3 : 2; + + List> chunks = new(); + for (int i = 1; i < maxCartridges + 1; i++) + { + PluginConfigColor color = (gauge.Ammo < i || gauge.Ammo - maxCartridges < i) ? mainColor : extraColor; + chunks.Add(new(color, i <= gauge.Ammo ? 1 : 0, null)); + } + + BarHud[] bars = BarUtilities.GetChunkedBars(Config.PowderGauge, chunks.ToArray(), player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.PowderGauge.StrataLevel)); + } + } + + private void DrawNoMercyBar(Vector2 origin, IPlayerCharacter player) + { + float noMercyDuration = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId == 1831 && o.RemainingTime > 0f)?.RemainingTime ?? 0f; + if (Config.NoMercy.HideWhenInactive && noMercyDuration <= 0) + { + return; + } + + Config.NoMercy.Label.SetValue(noMercyDuration); + + BarHud bar = BarUtilities.GetProgressBar(Config.NoMercy, noMercyDuration, 20f, 0f, player); + AddDrawActions(bar.GetDrawActions(origin, Config.NoMercy.StrataLevel)); + } + } + + [Section("Job Specific Bars")] + [SubSection("Tank", 0)] + [SubSection("Gunbreaker", 1)] + public class GunbreakerConfig : JobConfig + { + [JsonIgnore] public override uint JobId => JobIDs.GNB; + public new static GunbreakerConfig DefaultConfig() { return new GunbreakerConfig(); } + + [NestedConfig("Powder Gauge", 30)] + public PowderGauge PowderGauge = new PowderGauge( + new(0, -32), + new(254, 20), + new(new Vector4(0f / 255f, 162f / 255f, 252f / 255f, 1f)) + ); + + [NestedConfig("No Mercy", 35)] + public ProgressBarConfig NoMercy = new ProgressBarConfig( + new(0, -10), + new(254, 20), + new(new Vector4(252f / 255f, 204f / 255f, 255f / 255f, 1f)) + ); + } + + public class PowderGauge : ChunkedBarConfig + { + [ColorEdit4("Bloodfest Extra Cartridges Color")] + [Order(26)] + public PluginConfigColor BloodfestExtraCartridgesColor = new(new Vector4(240f / 255f, 200f / 255f, 0, 1)); + + public PowderGauge(Vector2 position, Vector2 size, PluginConfigColor fillColor, int padding = 2) : base(position, size, fillColor, padding) + { + } + } +} \ No newline at end of file diff --git a/Interface/Jobs/JobConfig.cs b/Interface/Jobs/JobConfig.cs new file mode 100644 index 0000000..8ef0de6 --- /dev/null +++ b/Interface/Jobs/JobConfig.cs @@ -0,0 +1,41 @@ +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Helpers; +using Newtonsoft.Json; +using System; +using System.Reflection; + +namespace HSUI.Interface.Jobs +{ + public abstract class JobConfig : MovablePluginConfigObject + { + [JsonIgnore] + public abstract uint JobId { get; } + + [Checkbox("Show Generic Mana Bar")] + [Order(20)] + public bool UseDefaultPrimaryResourceBar = false; + + [NestedConfig("Visibility", 2000)] + public VisibilityConfig VisibilityConfig = new VisibilityConfig(); + + [JsonIgnore] + public PrimaryResourceTypes PrimaryResourceType = PrimaryResourceTypes.MP; + + public new static JobConfig? DefaultConfig() + { + var type = MethodBase.GetCurrentMethod()?.DeclaringType; + if (type is null) + { + return null; + } + + return (JobConfig?)Activator.CreateInstance(type); + } + + public JobConfig() + { + Position.Y = HUDConstants.JobHudsBaseY; + } + } +} diff --git a/Interface/Jobs/JobHud.cs b/Interface/Jobs/JobHud.cs new file mode 100644 index 0000000..7aa39ab --- /dev/null +++ b/Interface/Jobs/JobHud.cs @@ -0,0 +1,37 @@ +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Plugin; +using System.Numerics; + +namespace HSUI.Interface.Jobs +{ + public class JobHud : DraggableHudElement, IHudElementWithActor, IHudElementWithVisibilityConfig + { + protected IDalamudPluginInterface PluginInterface => Plugin.PluginInterface; + + public JobConfig Config => (JobConfig)_config; + public VisibilityConfig VisibilityConfig => Config.VisibilityConfig; + + public IGameObject? Actor { get; set; } = null; + protected IPlayerCharacter? Player => Actor is IPlayerCharacter ? (IPlayerCharacter)Actor : null; + + public JobHud(JobConfig config, string? displayName = null) : base(config, displayName) + { + } + + public override void DrawChildren(Vector2 origin) + { + if (Player == null || !_config.Enabled) + { + return; + } + + DrawJobHud(origin, Player); + } + + public virtual void DrawJobHud(Vector2 origin, IPlayerCharacter player) + { + // override + } + } +} diff --git a/Interface/Jobs/MachinistHud.cs b/Interface/Jobs/MachinistHud.cs new file mode 100644 index 0000000..849e612 --- /dev/null +++ b/Interface/Jobs/MachinistHud.cs @@ -0,0 +1,257 @@ +using Dalamud.Game.ClientState.JobGauge.Types; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Logging; +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Enums; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using HSUI.Interface.GeneralElements; +using Newtonsoft.Json; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace HSUI.Interface.Jobs +{ + public class MachinistHud : JobHud + { + private bool _robotMaxDurationSet; + private float _robotMaxDuration; + + private new MachinistConfig Config => (MachinistConfig)_config; + + public MachinistHud(MachinistConfig config, string? displayName = null) : base(config, displayName) + { + } + + protected override (List, List) ChildrenPositionsAndSizes() + { + List positions = new List(); + List sizes = new List(); + + if (Config.OverheatChunkedGauge.Enabled) + { + positions.Add(Config.Position + Config.OverheatChunkedGauge.Position); + sizes.Add(Config.OverheatChunkedGauge.Size); + } + + if (Config.HeatGauge.Enabled) + { + positions.Add(Config.Position + Config.HeatGauge.Position); + sizes.Add(Config.HeatGauge.Size); + } + + if (Config.BatteryGauge.Enabled) + { + positions.Add(Config.Position + Config.BatteryGauge.Position); + sizes.Add(Config.BatteryGauge.Size); + } + + return (positions, sizes); + } + + public override void DrawJobHud(Vector2 origin, IPlayerCharacter player) + { + Vector2 pos = origin + Config.Position; + + if (Config.OverheatChunkedGauge.Enabled) + { + DrawOverheatBar(pos, player); + } + + if (Config.HeatGauge.Enabled) + { + DrawHeatGauge(pos, player); + } + + if (Config.BatteryGauge.Enabled) + { + DrawBatteryGauge(pos, player); + } + + if (Config.AutomatonBar.Enabled) + { + DrawAutomatonBar(pos, player); + } + + if (Config.WildfireBar.Enabled) + { + DrawWildfireBar(pos, player); + } + } + + private void DrawHeatGauge(Vector2 origin, IPlayerCharacter player) + { + MCHGauge gauge = Plugin.JobGauges.Get(); + + if (!Config.HeatGauge.HideWhenInactive || gauge.Heat > 0) + { + Config.HeatGauge.Label.SetValue(gauge.Heat); + + BarHud[] bars = BarUtilities.GetChunkedProgressBars(Config.HeatGauge, 2, gauge.Heat, 100, 0, player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.HeatGauge.StrataLevel)); + } + } + } + + private void DrawBatteryGauge(Vector2 origin, IPlayerCharacter player) + { + MCHGauge gauge = Plugin.JobGauges.Get(); + + if (!Config.BatteryGauge.HideWhenInactive || gauge.Battery > 0) + { + Config.BatteryGauge.Label.SetValue(gauge.Battery); + + BarHud bar = BarUtilities.GetProgressBar(Config.BatteryGauge, gauge.Battery, 100f, 0f, player, Config.BatteryGauge.BatteryColor); + AddDrawActions(bar.GetDrawActions(origin, Config.BatteryGauge.StrataLevel)); + } + } + + private void DrawAutomatonBar(Vector2 origin, IPlayerCharacter player) + { + MCHGauge gauge = Plugin.JobGauges.Get(); + + if (!gauge.IsRobotActive && _robotMaxDurationSet) + { + _robotMaxDurationSet = false; + } + + if (!Config.AutomatonBar.HideWhenInactive || gauge.IsRobotActive) + { + float remaining = gauge.SummonTimeRemaining / 1000f; + if (!_robotMaxDurationSet && gauge.IsRobotActive) + { + _robotMaxDuration = remaining; + _robotMaxDurationSet = true; + } + + Config.AutomatonBar.Label.SetValue(remaining); + + BarHud bar = BarUtilities.GetProgressBar(Config.AutomatonBar, remaining, _robotMaxDuration > 0 ? _robotMaxDuration : 1f, 0f, player); + AddDrawActions(bar.GetDrawActions(origin, Config.AutomatonBar.StrataLevel)); + } + } + + private void DrawOverheatBar(Vector2 origin, IPlayerCharacter player) + { + MCHGauge gauge = Plugin.JobGauges.Get(); + ushort stackCount = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 2688)?.Param ?? 0; + + if (!Config.OverheatChunkedGauge.HideWhenInactive || stackCount > 0) + { + LabelConfig[]? labels = null; + if (Config.OverheatChunkedGauge.DurationLabel.Enabled) + { + Config.OverheatChunkedGauge.DurationLabel.SetValue(gauge.OverheatTimeRemaining / 1000f); + labels = new LabelConfig[5]; + labels[2] = Config.OverheatChunkedGauge.DurationLabel; + } + + BarHud[] bars = BarUtilities.GetChunkedBars(Config.OverheatChunkedGauge, 5, stackCount, 5, labels: labels ); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.OverheatChunkedGauge.StrataLevel)); + } + } + } + + private void DrawWildfireBar(Vector2 origin, IPlayerCharacter player) + { + float wildfireDuration = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 1946)?.RemainingTime ?? 0f; + + if (!Config.WildfireBar.HideWhenInactive || wildfireDuration > 0) + { + Config.WildfireBar.Label.SetValue(wildfireDuration); + + BarHud bar = BarUtilities.GetProgressBar(Config.WildfireBar, wildfireDuration, 10, 0f, player); + AddDrawActions(bar.GetDrawActions(origin, Config.WildfireBar.StrataLevel)); + } + } + } + + [Section("Job Specific Bars")] + [SubSection("Ranged", 0)] + [SubSection("Machinist", 1)] + public class MachinistConfig : JobConfig + { + [JsonIgnore] public override uint JobId => JobIDs.MCH; + public new static MachinistConfig DefaultConfig() + { + var config = new MachinistConfig(); + + config.HeatGauge.UsePartialFillColor = true; + + return config; + } + + [NestedConfig("Overheat Gauge", 30)] + public MachinistOverheatBarConfig OverheatChunkedGauge = new MachinistOverheatBarConfig( + new Vector2(0, -54), + new Vector2(254, 20), + new PluginConfigColor(new Vector4(255f / 255f, 239f / 255f, 14f / 255f, 100f / 100f)) + ); + + [NestedConfig("Heat Gauge", 35)] + public ChunkedProgressBarConfig HeatGauge = new ChunkedProgressBarConfig( + new Vector2(0, -32), + new Vector2(254, 20), + new PluginConfigColor(new Vector4(201f / 255f, 13f / 255f, 13f / 255f, 100f / 100f)), + 2, + new PluginConfigColor(new Vector4(180f / 255f, 180f / 255f, 180f / 255f, 100f / 100f)) + ); + + [NestedConfig("Battery Gauge", 40)] + public MachinistBatteryGaugeConfig BatteryGauge = new MachinistBatteryGaugeConfig( + new Vector2(-31, -10), + new Vector2(192, 20), + new PluginConfigColor(new Vector4(0, 0, 0, 0)) + ); + + [NestedConfig("Automaton Queen Bar", 45)] + public ProgressBarConfig AutomatonBar = new ProgressBarConfig( + new Vector2(97, -10), + new Vector2(60, 20), + new PluginConfigColor(new Vector4(153f / 255f, 0f / 255f, 255f / 255f, 100f / 100f)) + ); + + [NestedConfig("Wildfire Bar", 50)] + public ProgressBarConfig WildfireBar = new ProgressBarConfig( + new Vector2(0, -76), + new Vector2(254, 20), + new PluginConfigColor(new Vector4(255f / 255f, 0f / 255f, 0f / 255f, 100f / 100f)), + BarDirection.Right, + new PluginConfigColor(new Vector4(180f / 255f, 180f / 255f, 180f / 255f, 100f / 100f)), + 50 + ); + } + + [DisableParentSettings("FillColor")] + [Exportable(false)] + public class MachinistBatteryGaugeConfig : ProgressBarConfig + { + [ColorEdit4("Battery Color", spacing = true)] + [Order(55)] + public PluginConfigColor BatteryColor = new(new Vector4(106f / 255f, 255f / 255f, 255f / 255f, 100f / 100f)); + + public MachinistBatteryGaugeConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) : base(position, size, fillColor) + { + } + } + + [Exportable(false)] + public class MachinistOverheatBarConfig : ChunkedBarConfig + { + [NestedConfig("Duration Text", 1000)] + public NumericLabelConfig DurationLabel; + + public MachinistOverheatBarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) + : base(position, size, fillColor) + { + DurationLabel = new NumericLabelConfig(Vector2.Zero, "", DrawAnchor.Center, DrawAnchor.Center); + DurationLabel.HideIfZero = true; + } + } +} \ No newline at end of file diff --git a/Interface/Jobs/MonkHud.cs b/Interface/Jobs/MonkHud.cs new file mode 100644 index 0000000..b354ff0 --- /dev/null +++ b/Interface/Jobs/MonkHud.cs @@ -0,0 +1,464 @@ +using Dalamud.Game.ClientState.JobGauge.Enums; +using Dalamud.Game.ClientState.JobGauge.Types; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Statuses; +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Enums; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using HSUI.Interface.GeneralElements; +using FFXIVClientStructs.FFXIV.Client.Game; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace HSUI.Interface.Jobs +{ + public class MonkHud : JobHud + { + private new MonkConfig Config => (MonkConfig)_config; + + private string[] _chunkTexts = new string[] { "I", "II", "III" }; + + public MonkHud(MonkConfig config, string? displayName = null) : base(config, displayName) + { + } + + protected override (List, List) ChildrenPositionsAndSizes() + { + List positions = new List(); + List sizes = new List(); + + if (Config.ChakraBar.Enabled) + { + positions.Add(Config.Position + Config.ChakraBar.Position); + sizes.Add(Config.ChakraBar.Size); + } + + if (Config.BeastChakraStacksBar.Enabled) + { + positions.Add(Config.Position + Config.BeastChakraStacksBar.Position); + sizes.Add(Config.BeastChakraStacksBar.Size); + } + + if (Config.MastersGauge.Enabled) + { + positions.Add(Config.Position + Config.MastersGauge.Position); + sizes.Add(Config.MastersGauge.Size); + } + + if (Config.StancesBar.Enabled) + { + positions.Add((Config.Position + Config.StancesBar.Position)); + sizes.Add(Config.StancesBar.Size); + } + + if (Config.PerfectBalanceBar.Enabled) + { + positions.Add(Config.Position + Config.PerfectBalanceBar.Position); + sizes.Add(Config.PerfectBalanceBar.Size); + } + + return (positions, sizes); + } + + public override void DrawJobHud(Vector2 origin, IPlayerCharacter player) + { + var position = origin + Config.Position; + + if (Config.ChakraBar.Enabled) + { + DrawChakraGauge(position, player); + } + + if (Config.BeastChakraStacksBar.Enabled) + { + DrawBeastChakraStacksBar(position, player); + } + + if (Config.MastersGauge.Enabled) + { + DrawMastersGauge(position, player); + } + + if (Config.StancesBar.Enabled) + { + DrawFormsBar(position, player); + } + + if (Config.PerfectBalanceBar.Enabled) + { + DrawPerfectBalanceBar(position, player); + } + } + + private void DrawChakraGauge(Vector2 origin, IPlayerCharacter player) + { + MNKGauge gauge = Plugin.JobGauges.Get(); + if (Config.ChakraBar.HideWhenInactive && gauge.Chakra == 0) + { + return; + } + + PluginConfigColor mainColor = Config.ChakraBar.FillColor; + PluginConfigColor extraColor = Config.ChakraBar.BrotherhoodExtraCharkaColor; + + List> chunks = new(); + for (int i = 1; i < 6; i++) + { + PluginConfigColor color = (gauge.Chakra < i || gauge.Chakra - 5 < i) ? mainColor : extraColor; + chunks.Add(new(color, i <= gauge.Chakra ? 1 : 0, null)); + } + + BarHud[] bars = BarUtilities.GetChunkedBars(Config.ChakraBar, chunks.ToArray(), player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.ChakraBar.StrataLevel)); + } + } + + private unsafe void DrawBeastChakraStacksBar(Vector2 origin, IPlayerCharacter player) + { + MonkBeastChakraStacksBar config = Config.BeastChakraStacksBar; + MNKGauge gauge = Plugin.JobGauges.Get(); + int stacks = gauge.OpoOpoFury + gauge.RaptorFury + gauge.CoeurlFury; + + if (config.HideWhenInactive && stacks == 0) + { + return; + } + + PluginConfigColor empty = PluginConfigColor.Empty; + Tuple[] chunks = + [ + new(gauge.OpoOpoFury > 0 ? config.OpoopoColor : empty, 1, null), + new(gauge.RaptorFury > 0 ? config.RaptorColor : empty, 1, null), + new(gauge.CoeurlFury > 0 ? config.CoeurlColor : empty, 1, null), + new(gauge.CoeurlFury > 1 ? config.CoeurlColor : empty, 1, null), + ]; + + BarHud[] bars = BarUtilities.GetChunkedBars(config, chunks, player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, config.StrataLevel)); + } + } + + private unsafe void DrawMastersGauge(Vector2 origin, IPlayerCharacter player) + { + MNKGauge gauge = Plugin.JobGauges.Get(); + + if (Config.MastersGauge.HideWhenInactive && + gauge.Nadi == Nadi.None && + gauge.BeastChakra[0] == BeastChakra.None && + gauge.BeastChakra[1] == BeastChakra.None && + gauge.BeastChakra[2] == BeastChakra.None) + { + return; + } + + int[] order = Config.MastersGauge.ChakraOrder; + int[] hasChakra = + [ + gauge.Nadi.HasFlag(Nadi.Lunar) ? 1 : 0, + gauge.BeastChakra[0] != BeastChakra.None ? 1 : 0, + gauge.BeastChakra[0] != BeastChakra.None ? 1 : 0, + gauge.BeastChakra[0] != BeastChakra.None ? 1 : 0, + gauge.Nadi.HasFlag(Nadi.Solar) ? 1 : 0, + ]; + + PluginConfigColor[] colors = new[] + { + Config.MastersGauge.LunarNadiColor, + GetChakraColor(gauge.BeastChakra[0]), + GetChakraColor(gauge.BeastChakra[1]), + GetChakraColor(gauge.BeastChakra[2]), + Config.MastersGauge.SolarNadiColor + }; + + var chunks = new Tuple[5]; + for (int i = 0; i < chunks.Length; i++) + { + chunks[i] = new(colors[order[i]], hasChakra[order[i]], i == 2 ? Config.MastersGauge.BlitzTimerLabel : null); + } + + Config.MastersGauge.BlitzTimerLabel.SetValue(gauge.BlitzTimeRemaining / 1000); + + BarHud[] bars = BarUtilities.GetChunkedBars(Config.MastersGauge, chunks, player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.MastersGauge.StrataLevel)); + } + } + + private void DrawFormsBar(Vector2 origin, IPlayerCharacter player) + { + // formless fist + IStatus? formlessFist = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId == 2513); + if (formlessFist != null) + { + float remaining = Math.Abs(formlessFist.RemainingTime); + + BarHud bar = BarUtilities.GetProgressBar( + Config.StancesBar, + null, + new LabelConfig[] { Config.StancesBar.FormlessFistLabel }, + remaining, + 30f, + 0, + player, + Config.StancesBar.FormlessFistColor + ); + + Config.StancesBar.FormlessFistLabel.SetValue(remaining); + + AddDrawActions(bar.GetDrawActions(origin, Config.StancesBar.StrataLevel)); + return; + } + + // forms + IStatus? form = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 107 or 108 or 109); + if (Config.StancesBar.HideWhenInactive && form is null) + { + return; + } + + int activeFormIndex = form != null ? (int)form.StatusId - 107 : -1; + PluginConfigColor[] chunkColors = new PluginConfigColor[] + { + Config.StancesBar.OpoOpoColor, + Config.StancesBar.RaptorColor, + Config.StancesBar.CoeurlColor + }; + + LabelConfig[] chunkLabels = new LabelConfig[] + { + Config.StancesBar.FormLabel.Clone(0), + Config.StancesBar.FormLabel.Clone(1), + Config.StancesBar.FormLabel.Clone(2) + }; + + var chunks = new Tuple[3]; + for (int i = 0; i < chunks.Length; i++) + { + LabelConfig label = chunkLabels[i]; + label.SetText(_chunkTexts[i]); + + chunks[i] = new(chunkColors[i], activeFormIndex == i ? 1 : 0, label); + } + + BarHud[] bars = BarUtilities.GetChunkedBars(Config.StancesBar, chunks, player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.StancesBar.StrataLevel)); + } + } + + private void DrawPerfectBalanceBar(Vector2 origin, IPlayerCharacter player) + { + IStatus? perfectBalance = Utils.StatusListForBattleChara(player).Where(o => o.StatusId is 110 && o.RemainingTime > 0f).FirstOrDefault(); + float duration = perfectBalance?.RemainingTime ?? 0f; + float stacks = perfectBalance?.Param ?? 0f; + + if (Config.PerfectBalanceBar.HideWhenInactive && duration <= 0) + { + return; + } + + Tuple[] chunks = new Tuple[3]; + for (int i = 0; i < chunks.Length; i++) + { + chunks[i] = new( + Config.PerfectBalanceBar.FillColor, + i < stacks ? 1f : 0, + i == 1 ? Config.PerfectBalanceBar.PerfectBalanceLabel : null + ); + } + + Config.PerfectBalanceBar.PerfectBalanceLabel.SetValue(duration); + + BarHud[] bars = BarUtilities.GetChunkedBars(Config.PerfectBalanceBar, chunks, player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.PerfectBalanceBar.StrataLevel)); + } + } + + private PluginConfigColor GetChakraColor(BeastChakra chakra) => chakra switch + { + BeastChakra.OpoOpo => Config.MastersGauge.OpoopoChakraColor, + BeastChakra.Raptor => Config.MastersGauge.RaptorChakraColor, + BeastChakra.Coeurl => Config.MastersGauge.CoeurlChakraColor, + _ => new PluginConfigColor(new(0, 0, 0, 0)) + }; + } + + [Section("Job Specific Bars")] + [SubSection("Melee", 0)] + [SubSection("Monk", 1)] + public class MonkConfig : JobConfig + { + [JsonIgnore] public override uint JobId => JobIDs.MNK; + + public new static MonkConfig DefaultConfig() + { + var config = new MonkConfig(); + + config.StancesBar.Enabled = false; + config.MastersGauge.BlitzTimerLabel.HideIfZero = true; + config.PerfectBalanceBar.PerfectBalanceLabel.HideIfZero = true; + + return config; + } + + [NestedConfig("Chakra Bar", 35)] + public ChakraBar ChakraBar = new ChakraBar( + new(0, -32), + new(254, 20), + new(new Vector4(204f / 255f, 115f / 255f, 0f, 100f / 100f)) + ); + + [NestedConfig("Fury Stacks Bar", 40)] + public MonkBeastChakraStacksBar BeastChakraStacksBar = new MonkBeastChakraStacksBar( + new(0, -32), + new(254, 20) + ); + + [NestedConfig("Masterful Blitz Bar", 45)] + public MastersGauge MastersGauge = new MastersGauge( + new(0, -54), + new(254, 20) + ); + + [NestedConfig("Forms Bar", 50)] + public MonkStancesBarConfig StancesBar = new MonkStancesBarConfig( + new(0, -98), + new(254, 20) + ); + + [NestedConfig("Perfect Balance Bar", 55)] + public PerfectBalanceBar PerfectBalanceBar = new PerfectBalanceBar( + new(0, -76), + new(254, 20), + new(new Vector4(150f / 255f, 255f / 255f, 255f / 255f, 100f / 100f)) + ); + } + + public class ChakraBar: ChunkedBarConfig + { + [ColorEdit4("Brotherhood Extra Chakra Color")] + [Order(26)] + public PluginConfigColor BrotherhoodExtraCharkaColor = new(new Vector4(204f / 255f, 0, 0, 1)); + + public ChakraBar(Vector2 position, Vector2 size, PluginConfigColor fillColor, int padding = 2) : base(position, size, fillColor, padding) + { + } + } + + public class PerfectBalanceBar : ChunkedBarConfig + { + [NestedConfig("Perfect Balance Duration Text", 50, spacing = true)] + public NumericLabelConfig PerfectBalanceLabel; + + public PerfectBalanceBar(Vector2 position, Vector2 size, PluginConfigColor fillColor, int padding = 2) : base(position, size, fillColor, padding) + { + PerfectBalanceLabel = new NumericLabelConfig(Vector2.Zero, "", DrawAnchor.Center, DrawAnchor.Center); + } + } + + [DisableParentSettings("FillColor", "FillDirection")] + public class MonkBeastChakraStacksBar : ChunkedBarConfig + { + [ColorEdit4("Opo-opo Color")] + [Order(19)] + public PluginConfigColor OpoopoColor = PluginConfigColor.FromHex(0xFFFFB3D3); + + [ColorEdit4("Raptor Color")] + [Order(20)] + public PluginConfigColor RaptorColor = PluginConfigColor.FromHex(0xFFBF89E5); + + [ColorEdit4("Coeurl Color")] + [Order(21)] + public PluginConfigColor CoeurlColor = PluginConfigColor.FromHex(0xFF9AE7C0); + + public MonkBeastChakraStacksBar(Vector2 position, Vector2 size) + : base(position, size, PluginConfigColor.Empty) + { + } + } + + [DisableParentSettings("FillColor", "FillDirection")] + public class MastersGauge : ChunkedBarConfig + { + [ColorEdit4("Lunar Nadi Color")] + [Order(19)] + public PluginConfigColor LunarNadiColor = PluginConfigColor.FromHex(0xFFDA87FF); + + [ColorEdit4("Solar Nadi Color")] + [Order(20)] + public PluginConfigColor SolarNadiColor = PluginConfigColor.FromHex(0xFFFFFFCA); + + [ColorEdit4("Opo-opo Color")] + [Order(21)] + public PluginConfigColor OpoopoChakraColor = PluginConfigColor.FromHex(0xFFC1527E); + + [ColorEdit4("Raptor Color")] + [Order(22)] + public PluginConfigColor RaptorChakraColor = PluginConfigColor.FromHex(0xFF8C67BA); + + [ColorEdit4("Coeurl Color")] + [Order(23)] + public PluginConfigColor CoeurlChakraColor = PluginConfigColor.FromHex(0xFF326D5A); + + [DragDropHorizontal("Chakra Order", "Lunar Nadi", "Chakra 1", "Chakra 2", "Chakra 3", "Solar Nadi")] + [Order(24)] + public int[] ChakraOrder = new int[] { 0, 1, 2, 3, 4 }; + + [NestedConfig("Blitz Timer Text", 50, spacing = true)] + public NumericLabelConfig BlitzTimerLabel; + + public MastersGauge(Vector2 position, Vector2 size) + : base(position, size, PluginConfigColor.Empty) + { + BlitzTimerLabel = new NumericLabelConfig(Vector2.Zero, "", DrawAnchor.Center, DrawAnchor.Center); + } + } + + [DisableParentSettings("FillColor")] + public class MonkStancesBarConfig : ChunkedBarConfig + { + [ColorEdit4("Opo-opo Color")] + [Order(19)] + public PluginConfigColor OpoOpoColor = PluginConfigColor.FromHex(0xFFFFB3D3); + + [ColorEdit4("Raptor Color")] + [Order(20)] + public PluginConfigColor RaptorColor = PluginConfigColor.FromHex(0xFFBF89E5); + + [ColorEdit4("Coeurl Color")] + [Order(21)] + public PluginConfigColor CoeurlColor = PluginConfigColor.FromHex(0xFF9AE7C0); + + [ColorEdit4("Formless Fist Color")] + [Order(22)] + public PluginConfigColor FormlessFistColor = PluginConfigColor.FromHex(0xFF514793); + + [NestedConfig("Form Number Text", 500, spacing = true)] + public LabelConfig FormLabel; + + [NestedConfig("Formless Fist Duration Text", 1000, separator = false, spacing = true)] + public NumericLabelConfig FormlessFistLabel; + + public MonkStancesBarConfig(Vector2 position, Vector2 size) + : base(position, size, PluginConfigColor.Empty) + { + FormLabel = new LabelConfig(Vector2.Zero, "", DrawAnchor.Center, DrawAnchor.Center); + + FormlessFistLabel = new NumericLabelConfig(Vector2.Zero, "", DrawAnchor.Center, DrawAnchor.Center); + FormlessFistLabel.Enabled = false; + } + } +} diff --git a/Interface/Jobs/NinjaHud.cs b/Interface/Jobs/NinjaHud.cs new file mode 100644 index 0000000..58aef20 --- /dev/null +++ b/Interface/Jobs/NinjaHud.cs @@ -0,0 +1,341 @@ +using Dalamud.Game.ClientState.JobGauge.Types; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Game.ClientState.Statuses; +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using HSUI.Interface.GeneralElements; +using FFXIVClientStructs.FFXIV.Client.Game.Gauge; +using Newtonsoft.Json; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace HSUI.Interface.Jobs +{ + public class NinjaHud : JobHud + { + private new NinjaConfig Config => (NinjaConfig)_config; + + public NinjaHud(NinjaConfig config, string? displayName = null) : base(config, displayName) { } + + protected override (List, List) ChildrenPositionsAndSizes() + { + List positions = new List(); + List sizes = new List(); + + if (Config.KazematoiBar.Enabled) + { + positions.Add(Config.Position + Config.KazematoiBar.Position); + sizes.Add(Config.KazematoiBar.Size); + } + + if (Config.NinkiBar.Enabled) + { + positions.Add(Config.Position + Config.NinkiBar.Position); + sizes.Add(Config.NinkiBar.Size); + } + + if (Config.TrickAttackBar.Enabled) + { + positions.Add(Config.Position + Config.TrickAttackBar.Position); + sizes.Add(Config.TrickAttackBar.Size); + } + + if (Config.SuitonBar.Enabled) + { + positions.Add(Config.Position + Config.SuitonBar.Position); + sizes.Add(Config.SuitonBar.Size); + } + + if (Config.MudraBar.Enabled) + { + positions.Add(Config.Position + Config.MudraBar.Position); + sizes.Add(Config.MudraBar.Size); + } + + return (positions, sizes); + } + + public override void DrawJobHud(Vector2 origin, IPlayerCharacter player) + { + var pos = origin + Config.Position; + if (Config.MudraBar.Enabled) + { + DrawMudraBars(pos, player); + } + + if (Config.KazematoiBar.Enabled) + { + DrawKazematoiBar(pos, player); + } + + if (Config.NinkiBar.Enabled) + { + DrawNinkiGauge(pos, player); + } + + if (Config.TrickAttackBar.Enabled) + { + DrawTrickAttackBar(pos, player); + } + + if (Config.SuitonBar.Enabled) + { + DrawSuitonBar(pos, player); + } + } + + public (bool, bool, bool) GetMudraBuffs(IPlayerCharacter? player, out IStatus? ninjutsuBuff, out IStatus? kassatsuBuff, out IStatus? tcjBuff) + { + ninjutsuBuff = null; + kassatsuBuff = null; + tcjBuff = null; + + if (player is not null) + { + var statusList = Utils.StatusListForBattleChara(player); + foreach (IStatus status in statusList) + { + if (status.StatusId == 496) { ninjutsuBuff = status; } + if (status.StatusId == 497) { kassatsuBuff = status; } + if (status.StatusId == 1186) { tcjBuff = status; } + } + } + + return (ninjutsuBuff is not null, kassatsuBuff is not null, tcjBuff is not null); + } + + private void DrawMudraBars(Vector2 origin, IPlayerCharacter player) + { + var (hasNinjutsuBuff, hasKassatsuBuff, hasTCJBuff) = GetMudraBuffs(player, out IStatus? ninjutsuBuff, out IStatus? kassatsuBuff, out IStatus? tcjBuff); + + int mudraStacks = SpellHelper.Instance.GetStackCount(2, 2259); + float mudraCooldown = SpellHelper.Instance.GetSpellCooldown(2259); + + float current = 0f; + float max = 0f; + + // For some reason, the mudras may be on cooldown before the "Mudra" buff is applied. + // Mudra stack count is set to -2 when a mudra is in the middle of its re-cast timer, so we can check for that instead. + bool inNinjutsu = mudraStacks == -2 || hasNinjutsuBuff; + + if (hasTCJBuff || hasKassatsuBuff || inNinjutsu) + { + if (hasTCJBuff) + { + max = 6f; + current = tcjBuff is null || tcjBuff.RemainingTime < 0 ? max : tcjBuff.RemainingTime; + Config.MudraBar.Label.SetText(GenerateNinjutsuText(tcjBuff?.Param ?? 0, hasKassatsuBuff, hasTCJBuff)); + } + else if (hasKassatsuBuff) + { + max = 15f; + current = kassatsuBuff is null || kassatsuBuff.RemainingTime < 0 ? max : kassatsuBuff.RemainingTime; + Config.MudraBar.Label.SetText("KASSATSU"); + } + + if (inNinjutsu) + { + max = 6f; + current = ninjutsuBuff is null || ninjutsuBuff.RemainingTime < 0 ? max : ninjutsuBuff.RemainingTime; + Config.MudraBar.Label.SetText(GenerateNinjutsuText(ninjutsuBuff?.Param ?? 0, hasKassatsuBuff, hasTCJBuff)); + } + + PluginConfigColor fillColor = hasTCJBuff ? Config.MudraBar.TCJBarColor : hasKassatsuBuff ? Config.MudraBar.KassatsuBarColor : Config.MudraBar.FillColor; + Rect foreground = BarUtilities.GetFillRect(Config.MudraBar.Position, Config.MudraBar.Size, Config.MudraBar.FillDirection, fillColor, current, max); + + BarHud bar = new BarHud(Config.MudraBar, player); + bar.AddForegrounds(foreground); + bar.AddLabels(Config.MudraBar.Label); + + AddDrawActions(bar.GetDrawActions(origin, Config.MudraBar.StrataLevel)); + } + else + { + max = 40f; + current = max - mudraCooldown; + + if (!Config.MudraBar.HideWhenInactive || current < max) + { + Config.MudraBar.Label.SetValue((max - current) % 20); + + BarHud[] bars = BarUtilities.GetChunkedProgressBars(Config.MudraBar, 2, current, max, 0f, player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.MudraBar.StrataLevel)); + } + } + } + } + + private unsafe void DrawKazematoiBar(Vector2 origin, IPlayerCharacter player) + { + NinjaGauge* gauge = (NinjaGauge*)Plugin.JobGauges.Get().Address; + int stacks = gauge->Kazematoi; + + if (Config.KazematoiBar.HideWhenInactive && stacks == 0) + { + return; + } + + BarHud[] bars = BarUtilities.GetChunkedBars(Config.KazematoiBar, 5, stacks, 5, 0, player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.KazematoiBar.StrataLevel)); + } + } + + private unsafe void DrawNinkiGauge(Vector2 origin, IPlayerCharacter player) + { + NinjaGauge* gauge = (NinjaGauge*)Plugin.JobGauges.Get().Address; + int ninki = gauge->Ninki; + + if (Config.NinkiBar.HideWhenInactive && ninki == 0) + { + return; + } + + Config.NinkiBar.Label.SetValue(ninki); + + BarHud[] bars = BarUtilities.GetChunkedProgressBars(Config.NinkiBar, 2, ninki, 100, 0, player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.NinkiBar.StrataLevel)); + } + } + + private void DrawTrickAttackBar(Vector2 origin, IPlayerCharacter player) + { + IGameObject? actor = Plugin.TargetManager.SoftTarget ?? Plugin.TargetManager.Target; + + float trickDuration = Utils.StatusListForActor(actor).FirstOrDefault( + o => o.StatusId is 3254 or 3906 && o.SourceId == player.GameObjectId && o.RemainingTime > 0 + )?.RemainingTime ?? 0f; + + if (Config.TrickAttackBar.HideWhenInactive && trickDuration == 0) + { + return; + } + + Config.TrickAttackBar.Label.SetValue(trickDuration); + + BarHud bar = BarUtilities.GetProgressBar(Config.TrickAttackBar, trickDuration, 15f, 0f, player); + AddDrawActions(bar.GetDrawActions(origin, Config.TrickAttackBar.StrataLevel)); + } + + private void DrawSuitonBar(Vector2 origin, IPlayerCharacter player) + { + float suitonDuration = Utils.StatusListForBattleChara(player).FirstOrDefault( + o => o.StatusId == 3848 && o.RemainingTime > 0 + )?.RemainingTime ?? 0f; + + if (Config.SuitonBar.HideWhenInactive && suitonDuration == 0) + { + return; + } + + Config.SuitonBar.Label.SetValue(suitonDuration); + + BarHud bar = BarUtilities.GetProgressBar(Config.SuitonBar, suitonDuration, 20f, 0f, player); + AddDrawActions(bar.GetDrawActions(origin, Config.SuitonBar.StrataLevel)); + } + + private string GenerateNinjutsuText(ushort param, bool haveKassatsuBuff, bool haveTCJBuff) + { + return param switch + { + 1 or 2 or 3 => "FUMA SHURIKEN", + 6 or 7 => haveKassatsuBuff ? "GOKA MEKKYAKU" : "KATON", + 9 or 11 => "RAITON", + 13 or 14 => haveKassatsuBuff ? "HYOSHO RANRYU" : "HYOTON", + 27 or 30 => "HUTON", + 39 or 45 => "DOTON", + 54 or 57 => "SUITON", + _ => haveTCJBuff ? "TEN CHI JIN" : "", + }; + } + } + + [Section("Job Specific Bars")] + [SubSection("Melee", 0)] + [SubSection("Ninja", 1)] + public class NinjaConfig : JobConfig + { + [JsonIgnore] public override uint JobId => JobIDs.NIN; + + public new static NinjaConfig DefaultConfig() + { + var config = new NinjaConfig(); + + config.MudraBar.Label.FontID = FontsConfig.DefaultMediumFontKey; + config.MudraBar.LabelMode = LabelMode.ActiveChunk; + + config.TrickAttackBar.Label.FontID = FontsConfig.DefaultMediumFontKey; + config.TrickAttackBar.Enabled = false; + + config.SuitonBar.Label.FontID = FontsConfig.DefaultMediumFontKey; + config.SuitonBar.Enabled = false; + + config.NinkiBar.UsePartialFillColor = true; + + return config; + } + + [NestedConfig("Mudra Bar", 30)] + public NinjaMudraBarConfig MudraBar = new NinjaMudraBarConfig( + new(0, -50), + new(254, 10), + new PluginConfigColor(new Vector4(211f / 255f, 166f / 255f, 75f / 242f, 100f / 100f)) + ); + + [NestedConfig("Kazematoi Bar", 35)] + public ChunkedBarConfig KazematoiBar = new ChunkedBarConfig( + new(0, -10), + new(254, 20), + PluginConfigColor.FromHex(0xFFABB6BD) + ); + + + [NestedConfig("Ninki Bar", 40)] + public ChunkedProgressBarConfig NinkiBar = new ChunkedProgressBarConfig( + new(0, -32), + new(254, 20), + new PluginConfigColor(new Vector4(137f / 255f, 82f / 255f, 236f / 255f, 100f / 100f)) + ); + + [NestedConfig("Trick Attack Bar", 45)] + public ProgressBarConfig TrickAttackBar = new ProgressBarConfig( + new(0, -63), + new(254, 10), + new PluginConfigColor(new Vector4(191f / 255f, 40f / 255f, 0f / 255f, 100f / 100f)) + ); + + [NestedConfig("Shadow Walker Bar", 50)] + public ProgressBarConfig SuitonBar = new ProgressBarConfig( + new(0, -75), + new(254, 10), + new PluginConfigColor(new Vector4(202f / 255f, 228f / 255f, 246f / 242f, 100f / 100f)) + ); + } + + [Exportable(false)] + public class NinjaMudraBarConfig : ChunkedProgressBarConfig + { + [ColorEdit4("Kassatsu Color", spacing = true)] + [Order(60)] + public PluginConfigColor KassatsuBarColor = new(new Vector4(239 / 255f, 123 / 255f, 222 / 242f, 100f / 100f)); + + [ColorEdit4("Ten Chi Jin Color")] + [Order(65)] + public PluginConfigColor TCJBarColor = new(new Vector4(181 / 255f, 33 / 255f, 41 / 242f, 100f / 100f)); + + public NinjaMudraBarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) : base(position, size, fillColor, 2) + { + Label.Enabled = true; + UsePartialFillColor = true; + } + } +} diff --git a/Interface/Jobs/PaladinHud.cs b/Interface/Jobs/PaladinHud.cs new file mode 100644 index 0000000..b6aa4fb --- /dev/null +++ b/Interface/Jobs/PaladinHud.cs @@ -0,0 +1,164 @@ +using Dalamud.Game.ClientState.JobGauge.Types; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Statuses; +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using HSUI.Interface.GeneralElements; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Security.Principal; + +namespace HSUI.Interface.Jobs +{ + public class PaladinHud : JobHud + { + private new PaladinConfig Config => (PaladinConfig)_config; + + public PaladinHud(PaladinConfig config, string? displayName = null) : base(config, displayName) + { + } + + protected override (List, List) ChildrenPositionsAndSizes() + { + List positions = new(); + List sizes = new(); + + if (Config.OathGauge.Enabled) + { + positions.Add(Config.Position + Config.OathGauge.Position); + sizes.Add(Config.OathGauge.Size); + } + + if (Config.FightOrFlightBar.Enabled) + { + positions.Add(Config.Position + Config.FightOrFlightBar.Position); + sizes.Add(Config.FightOrFlightBar.Size); + } + + if (Config.RequiescatStacksBar.Enabled) + { + positions.Add(Config.Position + Config.RequiescatStacksBar.Position); + sizes.Add(Config.RequiescatStacksBar.Size); + } + + return (positions, sizes); + } + + public override void DrawJobHud(Vector2 origin, IPlayerCharacter player) + { + Vector2 pos = origin + Config.Position; + + if (Config.OathGauge.Enabled) + { + DrawOathGauge(pos, player); + } + + if (Config.FightOrFlightBar.Enabled) + { + DrawFightOrFlightBar(pos, player); + } + + if (Config.RequiescatStacksBar.Enabled) + { + DrawRequiescatBar(pos, player); + } + } + + private void DrawOathGauge(Vector2 origin, IPlayerCharacter player) + { + PLDGauge gauge = Plugin.JobGauges.Get(); + + if (!Config.OathGauge.HideWhenInactive || gauge.OathGauge > 0) + { + Config.OathGauge.Label.SetValue(gauge.OathGauge); + + BarHud[] bars = BarUtilities.GetChunkedProgressBars(Config.OathGauge, 2, gauge.OathGauge, 100, 0, player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.OathGauge.StrataLevel)); + } + } + } + + private void DrawFightOrFlightBar(Vector2 origin, IPlayerCharacter player) + { + float fightOrFlightDuration = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 76)?.RemainingTime ?? 0f; + + if (!Config.FightOrFlightBar.HideWhenInactive || fightOrFlightDuration > 0) + { + Config.FightOrFlightBar.Label.SetValue(fightOrFlightDuration); + + BarHud bar = BarUtilities.GetProgressBar(Config.FightOrFlightBar, fightOrFlightDuration, 20f, 0f, player); + AddDrawActions(bar.GetDrawActions(origin, Config.FightOrFlightBar.StrataLevel)); + } + } + + private void DrawRequiescatBar(Vector2 origin, IPlayerCharacter player) + { + IStatus? requiescatBuff = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 1368); + float requiescatDuration = Math.Max(0f, requiescatBuff?.RemainingTime ?? 0f); + int stacks = requiescatBuff?.Param ?? 0; + + if (!Config.RequiescatStacksBar.HideWhenInactive || requiescatDuration > 0) + { + Config.RequiescatStacksBar.Label.SetValue(requiescatDuration); + + LabelConfig[] labels = new LabelConfig[4]; + labels[2] = Config.RequiescatStacksBar.Label; + + BarHud[] bars = BarUtilities.GetChunkedBars(Config.RequiescatStacksBar, 4, stacks, 4, labels: labels); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.RequiescatStacksBar.StrataLevel)); + } + } + } + } + + [Section("Job Specific Bars")] + [SubSection("Tank", 0)] + [SubSection("Paladin", 1)] + public class PaladinConfig : JobConfig + { + [JsonIgnore] public override uint JobId => JobIDs.PLD; + + public new static PaladinConfig DefaultConfig() + { + var config = new PaladinConfig(); + + config.UseDefaultPrimaryResourceBar = true; + config.OathGauge.UsePartialFillColor = true; + config.RequiescatStacksBar.Label.Enabled = true; + + return config; + } + + [NestedConfig("Oath Gauge", 35)] + public ChunkedProgressBarConfig OathGauge = new ChunkedProgressBarConfig( + new Vector2(0, -54), + new Vector2(254, 20), + new PluginConfigColor(new Vector4(24f / 255f, 80f / 255f, 175f / 255f, 100f / 100f)), + 2, + new PluginConfigColor(new Vector4(180f / 255f, 180f / 255f, 180f / 255f, 100f / 100f)) + ); + + [NestedConfig("Fight or Flight Bar", 40)] + public ProgressBarConfig FightOrFlightBar = new ProgressBarConfig( + new Vector2(-64, -32), + new Vector2(126, 20), + new PluginConfigColor(new Vector4(240f / 255f, 50f / 255f, 0f / 255f, 100f / 100f)) + ); + + [NestedConfig("Requiescat Bar", 45)] + public ChunkedProgressBarConfig RequiescatStacksBar = new ChunkedProgressBarConfig( + new Vector2(64, -32), + new Vector2(126, 20), + new PluginConfigColor(new Vector4(61f / 255f, 61f / 255f, 255f / 255f, 100f / 100f)) + ); + } +} diff --git a/Interface/Jobs/PictomancerHud.cs b/Interface/Jobs/PictomancerHud.cs new file mode 100644 index 0000000..966e0f6 --- /dev/null +++ b/Interface/Jobs/PictomancerHud.cs @@ -0,0 +1,537 @@ +using Dalamud.Game.ClientState.JobGauge.Enums; +using Dalamud.Game.ClientState.JobGauge.Types; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Statuses; +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Enums; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using HSUI.Interface.GeneralElements; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text.Json.Serialization; + +namespace HSUI.Interface.Jobs +{ + public class PictomancerHud : JobHud + { + private new PictomancerConfig Config => (PictomancerConfig)_config; + + private static PluginConfigColor EmptyColor => GlobalColors.Instance.EmptyColor; + + public PictomancerHud(PictomancerConfig config, string? displayName = null) : base(config, displayName) + { + + } + + protected override (List, List) ChildrenPositionsAndSizes() + { + List positions = new List(); + List sizes = new List(); + + if (Config.PaletteBar.Enabled) + { + positions.Add(Config.Position + Config.PaletteBar.Position); + sizes.Add(Config.PaletteBar.Size); + } + + if (Config.PaintBar.Enabled) + { + positions.Add(Config.Position + Config.PaintBar.Position); + sizes.Add(Config.PaintBar.Size); + } + + if (Config.CreatureCanvasBar.Enabled) + { + positions.Add(Config.Position + Config.CreatureCanvasBar.Position); + sizes.Add(Config.CreatureCanvasBar.Size); + } + + if (Config.WeaponCanvasBar.Enabled) + { + positions.Add(Config.Position + Config.WeaponCanvasBar.Position); + sizes.Add(Config.WeaponCanvasBar.Size); + } + + if (Config.LandscapeCanvasBar.Enabled) + { + positions.Add(Config.Position + Config.LandscapeCanvasBar.Position); + sizes.Add(Config.LandscapeCanvasBar.Size); + } + + if (Config.HammerTimeBar.Enabled) + { + positions.Add(Config.Position + Config.HammerTimeBar.Position); + sizes.Add(Config.HammerTimeBar.Size); + } + + if (Config.HyperphantasiaBar.Enabled) + { + positions.Add(Config.Position + Config.HyperphantasiaBar.Position); + sizes.Add(Config.HyperphantasiaBar.Size); + } + + return (positions, sizes); + } + + public override void DrawJobHud(Vector2 origin, IPlayerCharacter player) + { + Vector2 pos = origin + Config.Position; + + if (Config.PaletteBar.Enabled) + { + DrawPaletteBar(pos, player); + } + + if (Config.PaintBar.Enabled) + { + DrawPaintBar(pos, player); + } + + if (Config.CreatureCanvasBar.Enabled) + { + DrawCreatureCanvasBar(pos, player); + } + + if (Config.WeaponCanvasBar.Enabled) + { + DrawWeaponCanvasBar(pos, player); + } + + if (Config.LandscapeCanvasBar.Enabled) + { + DrawLandscapeCanvasBar(pos, player); + } + + if (Config.HammerTimeBar.Enabled) + { + DrawHammerTimeBar(pos, player); + } + + if (Config.HyperphantasiaBar.Enabled) + { + DrawHyperphantasiaBar(pos, player); + } + } + + protected unsafe void DrawPaletteBar(Vector2 origin, IPlayerCharacter player) + { + PictomancerPaletteBarConfig config = Config.PaletteBar; + PCTGauge gauge = Plugin.JobGauges.Get(); + + if (config.HideWhenInactive && gauge.PalleteGauge == 0) + { + return; + } + + bool isSubstractive = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 3674) != null; + + config.Label = config.PaletteLabel; + config.PaletteLabel.SubtractiveMode = isSubstractive; + config.Label.SetValue(gauge.PalleteGauge); + + PluginConfigColor fillColor = isSubstractive ? config.SubtractiveColor : config.FillColor; + + BarHud[] bars = BarUtilities.GetChunkedProgressBars( + config, + 2, + gauge.PalleteGauge, + 100, + 0, + player, + fillColor: fillColor + ); + + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, config.StrataLevel)); + } + } + + private unsafe void DrawPaintBar(Vector2 origin, IPlayerCharacter player) + { + PictomancerPaintBarConfig config = Config.PaintBar; + PCTGauge gauge = Plugin.JobGauges.Get(); + + if (config.HideWhenInactive && gauge.Paint == 0) + { + return; + } + + bool hasBlackPaint = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 3691) != null; + Tuple[] chunks = new Tuple[5]; + + for (int i = 0; i < 5; i++) + { + PluginConfigColor color = PluginConfigColor.Empty; + if (i < gauge.Paint) + { + color = i == gauge.Paint - 1 && hasBlackPaint ? config.BlackPaintColor : config.WhitePaintColor; + } + chunks[i] = new(color, 1, null); + } + + BarHud[] bars = BarUtilities.GetChunkedBars(config, chunks, player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, config.StrataLevel)); + } + } + + private unsafe void DrawCreatureCanvasBar(Vector2 origin, IPlayerCharacter player) + { + PictomancerCreatureCanvasBarConfig config = Config.CreatureCanvasBar; + PCTGauge gauge = Plugin.JobGauges.Get(); + + if (config.HideWhenInactive && !gauge.CreatureMotifDrawn && gauge.CreatureFlags != 0) + { + return; + } + + // canvas + PluginConfigColor? canvasColor = null; + if (gauge.CanvasFlags.HasFlag(CanvasFlags.Maw)) + { + canvasColor = config.FangsColor; + } + else if (gauge.CanvasFlags.HasFlag(CanvasFlags.Claw)) + { + canvasColor = config.ClawColor; + } + else if (gauge.CanvasFlags.HasFlag(CanvasFlags.Wing)) + { + canvasColor = config.WingsColor; + } + else if(gauge.CanvasFlags.HasFlag(CanvasFlags.Pom)) + { + canvasColor = config.PomColor; + } + Tuple canvas = new( + canvasColor ?? PluginConfigColor.Empty, + canvasColor != null ? 1 : 0, + null + ); + + // part drawing + PluginConfigColor? drawingColor = null; + if (gauge.CreatureFlags.HasFlag(CreatureFlags.Claw)) + { + drawingColor = config.ClawColor; + } + else if (config.ShowEmptyPomWings && + gauge.CreatureFlags.HasFlag(CreatureFlags.Pom) && + gauge.CreatureFlags.HasFlag(CreatureFlags.Wings)) + { + drawingColor = PluginConfigColor.Empty; + } + else if (gauge.CreatureFlags.HasFlag(CreatureFlags.Wings)) + { + drawingColor = config.WingsColor; + } + else if(gauge.CreatureFlags.HasFlag(CreatureFlags.Pom)) + { + drawingColor = config.PomColor; + } + Tuple drawing = new( + drawingColor ?? PluginConfigColor.Empty, + drawingColor != null ? 1 : 0, + null + ); + + // portrait + PluginConfigColor? portraitColor = null; + if (gauge.CreatureFlags.HasFlag(CreatureFlags.MooglePortait)) + { + portraitColor = config.MoogleColor; + } + else if (gauge.CreatureFlags.HasFlag(CreatureFlags.MadeenPortrait)) + { + portraitColor = config.MadeenColor; + } + Tuple portrait = new( + portraitColor ?? PluginConfigColor.Empty, + portraitColor != null ? 1 : 0, + null + ); + + var chunks = new List>(); + chunks.Add(canvas); + if (!config.DontShowDrawing) + { + chunks.Add(drawing); + } + chunks.Add(portrait); + + BarHud[] bars = BarUtilities.GetChunkedBars(config, chunks.ToArray(), player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, config.StrataLevel)); + } + } + + private unsafe void DrawWeaponCanvasBar(Vector2 origin, IPlayerCharacter player) + { + ChunkedBarConfig config = Config.WeaponCanvasBar; + PCTGauge gauge = Plugin.JobGauges.Get(); + + bool active = gauge.CanvasFlags.HasFlag(CanvasFlags.Weapon); + + if (config.HideWhenInactive && !active) + { + return; + } + + BarHud[] bars = BarUtilities.GetChunkedBars(config, 1, active ? 1 : 0, 1, 0, player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, config.StrataLevel)); + } + } + + private unsafe void DrawLandscapeCanvasBar(Vector2 origin, IPlayerCharacter player) + { + ChunkedBarConfig config = Config.LandscapeCanvasBar; + PCTGauge gauge = Plugin.JobGauges.Get(); + + bool active = gauge.CanvasFlags.HasFlag(CanvasFlags.Landscape); + + if (config.HideWhenInactive && !active) + { + return; + } + + BarHud[] bars = BarUtilities.GetChunkedBars(config, 1, active ? 1 : 0, 1, 0, player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, config.StrataLevel)); + } + } + + private unsafe void DrawHammerTimeBar(Vector2 origin, IPlayerCharacter player) + { + StacksWithDurationBarConfig config = Config.HammerTimeBar; + IStatus? status = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 3680); + int stacks = status?.Param ?? 0; + float time = Math.Abs(status?.RemainingTime ?? 0f); + + if (config.HideWhenInactive && stacks == 0) + { + return; + } + + config.Label.SetValue(time); + + BarHud[] bars = BarUtilities.GetChunkedProgressBars(config, 3, stacks, 3, 0, player, forceLabelIndex: 1); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, config.StrataLevel)); + } + } + + private unsafe void DrawHyperphantasiaBar(Vector2 origin, IPlayerCharacter player) + { + StacksWithDurationBarConfig config = Config.HyperphantasiaBar; + IStatus? status = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 3688); + int stacks = status?.Param ?? 0; + float time = Math.Abs(status?.RemainingTime ?? 0f); + + if (config.HideWhenInactive && stacks == 0) + { + return; + } + + config.Label.SetValue(time); + + BarHud[] bars = BarUtilities.GetChunkedProgressBars(config, 5, stacks, 5, 0, player, forceLabelIndex: 2); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, config.StrataLevel)); + } + } + } + + [Section("Job Specific Bars")] + [SubSection("Caster", 0)] + [SubSection("Pictomancer", 1)] + public class PictomancerConfig : JobConfig + { + [JsonIgnore] public override uint JobId => JobIDs.PCT; + + public new static PictomancerConfig DefaultConfig() + { + var config = new PictomancerConfig(); + config.PaletteBar.UseChunks = false; + config.PaletteBar.Label.Enabled = true; + + return config; + } + + [NestedConfig("Palette Bar", 30)] + public PictomancerPaletteBarConfig PaletteBar = new PictomancerPaletteBarConfig( + new Vector2(0, -10), + new Vector2(152, 18), + PluginConfigColor.FromHex(0xFFC8C89F) + ); + + [NestedConfig("Paint Bar", 35)] + public PictomancerPaintBarConfig PaintBar = new PictomancerPaintBarConfig( + new(0, -26), + new(254, 10) + ); + + [NestedConfig("Creature Canvas Bar", 40)] + public PictomancerCreatureCanvasBarConfig CreatureCanvasBar = new PictomancerCreatureCanvasBarConfig( + new(0, -38), + new(254, 10) + ); + + [NestedConfig("Weapon Canvas Bar", 40)] + public ChunkedBarConfig WeaponCanvasBar = new ChunkedBarConfig( + new(-102.5f, -10), + new(49, 18), + PluginConfigColor.FromHex(0xFFC5616C) + ); + + [NestedConfig("Landscape Canvas Bar", 40)] + public ChunkedBarConfig LandscapeCanvasBar = new ChunkedBarConfig( + new(102.5f, -10), + new(49, 18), + PluginConfigColor.FromHex(0xFF8690E5) + ); + + [NestedConfig("Hammer Time Bar", 40)] + public StacksWithDurationBarConfig HammerTimeBar = new StacksWithDurationBarConfig( + new(0, -50), + new(254, 10), + PluginConfigColor.FromHex(0xFFFFFFFF) + ); + + [NestedConfig("Hyperphantasia Bar", 45)] + public StacksWithDurationBarConfig HyperphantasiaBar = new StacksWithDurationBarConfig( + new(0, -50), + new(254, 10), + PluginConfigColor.FromHex(0xFFFFFFFF) + ); + } + + [DisableParentSettings("Label")] + [Exportable(false)] + public class PictomancerPaletteBarConfig : ChunkedProgressBarConfig + { + [ColorEdit4("Subtractive Fill Color")] + [Order(27)] + public PluginConfigColor SubtractiveColor = PluginConfigColor.FromHex(0xFFAF6BAE); + + [NestedConfig("Bar Text", 1001, separator = false, spacing = true)] + public PictomancerPaletteLabelConfig PaletteLabel = new PictomancerPaletteLabelConfig(Vector2.Zero, "", DrawAnchor.Center, DrawAnchor.Center); + + public PictomancerPaletteBarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) + : base(position, size, fillColor) + { + Label = PaletteLabel; + } + } + + [Exportable(false)] + public class PictomancerPaletteLabelConfig : NumericLabelConfig + { + [ColorEdit4("Subtractive Color")] + [Order(31)] + public PluginConfigColor SubtractiveColor = new PluginConfigColor(Vector4.UnitW); + + [ColorEdit4("Subtractive Color ##Outline")] + [Order(41, collapseWith = nameof(ShowOutline))] + public PluginConfigColor SubtractiveOutlineColor = new PluginConfigColor(Vector4.One); + + public PictomancerPaletteLabelConfig(Vector2 position, string text, DrawAnchor frameAnchor, DrawAnchor textAnchor) + : base(position, text, frameAnchor, textAnchor) + { + } + + [JsonIgnore] public bool SubtractiveMode = false; + + public override PluginConfigColor GetColor() => SubtractiveMode ? SubtractiveColor : Color; + + public override PluginConfigColor GetOutlineColor() => SubtractiveMode ? SubtractiveOutlineColor : OutlineColor; + + public override PictomancerPaletteLabelConfig Clone(int index) => + new PictomancerPaletteLabelConfig(Position, _text, FrameAnchor, TextAnchor) + { + Color = Color, + SubtractiveColor = SubtractiveColor, + OutlineColor = OutlineColor, + SubtractiveOutlineColor = SubtractiveOutlineColor, + ShadowConfig = ShadowConfig, + ShowOutline = ShowOutline, + FontID = FontID, + UseJobColor = UseJobColor, + Enabled = Enabled, + HideIfZero = HideIfZero, + ID = ID + "_{index}" + }; + } + + [DisableParentSettings("FillColor")] + [Exportable(false)] + public class PictomancerPaintBarConfig : ChunkedBarConfig + { + [ColorEdit4("White Paint Color")] + [Order(26)] + public PluginConfigColor WhitePaintColor = PluginConfigColor.FromHex(0xFF00FFFF); + + [ColorEdit4("Black Paint Color")] + [Order(27)] + public PluginConfigColor BlackPaintColor = PluginConfigColor.FromHex(0xFFDB57DB); + + public PictomancerPaintBarConfig(Vector2 position, Vector2 size) + : base(position, size, PluginConfigColor.Empty) + { + } + } + + [DisableParentSettings("FillColor")] + [Exportable(false)] + public class PictomancerCreatureCanvasBarConfig : ChunkedBarConfig + { + [ColorEdit4("Pom Color")] + [Order(17)] + public PluginConfigColor PomColor = PluginConfigColor.FromHex(0xFFE69378); + + [ColorEdit4("Wings Color")] + [Order(18)] + public PluginConfigColor WingsColor = PluginConfigColor.FromHex(0xFFD38BE4); + + [ColorEdit4("Claw Color")] + [Order(19)] + public PluginConfigColor ClawColor = PluginConfigColor.FromHex(0xFFA16854); + + [ColorEdit4("Fangs Color")] + [Order(20)] + public PluginConfigColor FangsColor = PluginConfigColor.FromHex(0xFF80BFBD); + + [ColorEdit4("Moogle Color")] + [Order(21)] + public PluginConfigColor MoogleColor = PluginConfigColor.FromHex(0xFFA745C7); + + [ColorEdit4("Madeen Color")] + [Order(22)] + public PluginConfigColor MadeenColor = PluginConfigColor.FromHex(0xFF93Cf7D); + + [Checkbox("Don't Show Drawing", spacing = true, help = "When enabled, this hides the middle chunk that shows which creature parts were already drawn.")] + [Order(50)] + public bool DontShowDrawing = false; + + [Checkbox("Show Empty Drawing for Pom + Wings")] + [Order(51)] + public bool ShowEmptyPomWings = false; + + + public PictomancerCreatureCanvasBarConfig(Vector2 position, Vector2 size) + : base(position, size, PluginConfigColor.Empty) + { + } + } +} diff --git a/Interface/Jobs/ReaperHud.cs b/Interface/Jobs/ReaperHud.cs new file mode 100644 index 0000000..34d0113 --- /dev/null +++ b/Interface/Jobs/ReaperHud.cs @@ -0,0 +1,246 @@ +using Dalamud.Game.ClientState.JobGauge.Types; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Objects.Types; +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Enums; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using HSUI.Interface.GeneralElements; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace HSUI.Interface.Jobs +{ + public class ReaperHud : JobHud + { + private new ReaperConfig Config => (ReaperConfig)_config; + + public ReaperHud(JobConfig config, string? displayName = null) : base(config, displayName) + { + } + + protected override (List, List) ChildrenPositionsAndSizes() + { + List positions = new(); + List sizes = new(); + + if (Config.DeathsDesignBar.Enabled) + { + positions.Add(Config.Position + Config.DeathsDesignBar.Position); + sizes.Add(Config.DeathsDesignBar.Size); + } + + if (Config.SoulBar.Enabled) + { + positions.Add(Config.Position + Config.SoulBar.Position); + sizes.Add(Config.SoulBar.Size); + } + + if (Config.ShroudBar.Enabled) + { + positions.Add(Config.Position + Config.ShroudBar.Position); + sizes.Add(Config.ShroudBar.Size); + } + + if (Config.DeathGauge.Enabled) + { + positions.Add(Config.Position + Config.DeathGauge.Position); + sizes.Add(Config.DeathGauge.Size); + } + + return (positions, sizes); + } + + public override void DrawJobHud(Vector2 origin, IPlayerCharacter player) + { + Vector2 pos = origin + Config.Position; + RPRGauge gauge = Plugin.JobGauges.Get(); + + if (Config.DeathsDesignBar.Enabled) + { + DrawDeathsDesignBar(pos, player); + } + + if (Config.SoulBar.Enabled) + { + DrawSoulGauge(pos, gauge, player); + } + + if (Config.ShroudBar.Enabled) + { + DrawShroudGauge(pos, gauge, player); + } + + if (Config.DeathGauge.Enabled) + { + DrawDeathGauge(pos, gauge, player); + } + } + + private void DrawDeathsDesignBar(Vector2 origin, IPlayerCharacter player) + { + IGameObject? actor = Plugin.TargetManager.SoftTarget ?? Plugin.TargetManager.Target; + float duration = 0f; + + if (actor is IBattleChara target) + { + duration = Utils.StatusListForBattleChara(target).FirstOrDefault(o => o.StatusId is 2586 && o.SourceId == player.GameObjectId && o.RemainingTime > 0)?.RemainingTime ?? 0f; + } + + if (!Config.DeathsDesignBar.HideWhenInactive || duration > 0) + { + Config.DeathsDesignBar.Label.SetValue(duration); + + BarHud[] bars = BarUtilities.GetChunkedProgressBars(Config.DeathsDesignBar, 2, duration, 60f, 0f, player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.DeathsDesignBar.StrataLevel)); + } + } + } + + private void DrawSoulGauge(Vector2 origin, RPRGauge gauge, IPlayerCharacter player) + { + float soul = gauge.Soul; + + if (!Config.SoulBar.HideWhenInactive || soul > 0) + { + Config.SoulBar.Label.SetValue(soul); + + BarHud[] bars = BarUtilities.GetChunkedProgressBars(Config.SoulBar, 2, soul, 100f, 0f, player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.SoulBar.StrataLevel)); + } + } + } + + private void DrawShroudGauge(Vector2 origin, RPRGauge gauge, IPlayerCharacter player) + { + float shroud = gauge.Shroud; + + if (!Config.ShroudBar.HideWhenInactive || shroud > 0) + { + Config.ShroudBar.Label.SetValue(shroud); + + BarHud[] bars = BarUtilities.GetChunkedProgressBars(Config.ShroudBar, 2, shroud, 100f, 0f, player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.ShroudBar.StrataLevel)); + } + } + } + + private void DrawDeathGauge(Vector2 origin, RPRGauge gauge, IPlayerCharacter player) + { + var lemureShroud = gauge.LemureShroud; + var voidShroud = gauge.VoidShroud; + + if (!Config.DeathGauge.HideWhenInactive || gauge.EnshroudedTimeRemaining > 0) + { + var deathChunks = new Tuple[5]; + + int i = 0; + for (; i < lemureShroud && i < deathChunks.Length; i++) + { + deathChunks[i] = new(Config.DeathGauge.LemureShroudColor, 1f, i == 2 ? Config.DeathGauge.EnshroudTimerLabel : null); + } + + for (; i < lemureShroud + voidShroud && i < deathChunks.Length; i++) + { + deathChunks[i] = new(Config.DeathGauge.VoidShroudColor, 1f, i == 2 ? Config.DeathGauge.EnshroudTimerLabel : null); + } + + for (; i < deathChunks.Length; i++) + { + deathChunks[i] = new(Config.DeathGauge.VoidShroudColor, 0f, i == 2 ? Config.DeathGauge.EnshroudTimerLabel : null); + } + + Config.DeathGauge.EnshroudTimerLabel.SetValue(gauge.EnshroudedTimeRemaining / 1000); + + BarHud[] bars = BarUtilities.GetChunkedBars(Config.DeathGauge, deathChunks, player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.DeathGauge.StrataLevel)); + } + } + } + } + + [Section("Job Specific Bars")] + [SubSection("Melee", 0)] + [SubSection("Reaper", 1)] + public class ReaperConfig : JobConfig + { + [JsonIgnore] public override uint JobId => JobIDs.RPR; + + public new static ReaperConfig DefaultConfig() + { + var config = new ReaperConfig(); + config.DeathsDesignBar.UseChunks = false; + config.DeathsDesignBar.Label.Enabled = true; + + config.SoulBar.UseChunks = false; + config.SoulBar.Label.Enabled = true; + + config.ShroudBar.UseChunks = false; + config.ShroudBar.Label.Enabled = true; + + config.DeathGauge.EnshroudTimerLabel.HideIfZero = true; + + return config; + } + + [NestedConfig("Death's Design Bar", 35)] + public ChunkedProgressBarConfig DeathsDesignBar = new ChunkedProgressBarConfig( + new(0, -10), + new(254, 20), + new PluginConfigColor(new Vector4(145f / 255f, 0f / 255f, 25f / 255f, 100f / 100f)) + ); + + [NestedConfig("Soul Bar", 40)] + public ChunkedProgressBarConfig SoulBar = new ChunkedProgressBarConfig( + new(0, -32), + new(254, 20), + new PluginConfigColor(new Vector4(254f / 255f, 21f / 255f, 94f / 255f, 100f / 100f)) + ); + + [NestedConfig("Shroud Bar", 45)] + public ChunkedProgressBarConfig ShroudBar = new ChunkedProgressBarConfig( + new(0, -54), + new(254, 20), + new PluginConfigColor(new Vector4(0f / 255f, 176f / 255f, 196f / 255f, 100f / 100f)) + ); + + [NestedConfig("Death Gauge", 50)] + public DeathGauge DeathGauge = new DeathGauge( + new(0, -76), + new(254, 20), + new PluginConfigColor(new(0, 0, 0, 0)) + ); + } + + [DisableParentSettings("FillColor")] + public class DeathGauge : ChunkedBarConfig + { + [ColorEdit4("Lemure Shroud Color")] + [Order(21)] + public PluginConfigColor LemureShroudColor = new PluginConfigColor(new Vector4(0f / 255f, 176f / 255f, 196f / 255f, 100f / 100f)); + + [ColorEdit4("Void Shroud Color")] + [Order(22)] + public PluginConfigColor VoidShroudColor = new PluginConfigColor(new Vector4(150f / 255f, 90f / 255f, 144f / 255f, 100f / 100f)); + + [NestedConfig("Enshroud Duration Text", 50, spacing = true)] + public NumericLabelConfig EnshroudTimerLabel; + + public DeathGauge(Vector2 position, Vector2 size, PluginConfigColor fillColor, int padding = 2) : base(position, size, fillColor, padding) + { + EnshroudTimerLabel = new NumericLabelConfig(Vector2.Zero, "", DrawAnchor.Center, DrawAnchor.Center); + } + } +} diff --git a/Interface/Jobs/RedMageHud.cs b/Interface/Jobs/RedMageHud.cs new file mode 100644 index 0000000..7d620cf --- /dev/null +++ b/Interface/Jobs/RedMageHud.cs @@ -0,0 +1,319 @@ +using Dalamud.Game.ClientState.JobGauge.Types; +using Dalamud.Game.ClientState.Objects.SubKinds; +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Enums; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using HSUI.Interface.GeneralElements; +using Newtonsoft.Json; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace HSUI.Interface.Jobs +{ + public class RedMageHud : JobHud + { + private new RedMageConfig Config => (RedMageConfig)_config; + + public RedMageHud(RedMageConfig config, string? displayName = null) : base(config, displayName) + { + } + + protected override (List, List) ChildrenPositionsAndSizes() + { + List positions = new List(); + List sizes = new List(); + + if (Config.BalanceBar.Enabled) + { + positions.Add(Config.Position + Config.BalanceBar.Position); + sizes.Add(Config.BalanceBar.Size); + } + + if (Config.WhiteManaBar.Enabled) + { + positions.Add(Config.Position + Config.WhiteManaBar.Position); + sizes.Add(Config.WhiteManaBar.Size); + } + + if (Config.BlackManaBar.Enabled) + { + positions.Add(Config.Position + Config.BlackManaBar.Position); + sizes.Add(Config.BlackManaBar.Size); + } + + if (Config.ManaStacksBar.Enabled) + { + positions.Add(Config.Position + Config.ManaStacksBar.Position); + sizes.Add(Config.ManaStacksBar.Size); + } + + if (Config.DualcastBar.Enabled) + { + positions.Add(Config.Position + Config.DualcastBar.Position); + sizes.Add(Config.DualcastBar.Size); + } + + if (Config.VerstoneBar.Enabled) + { + positions.Add(Config.Position + Config.VerstoneBar.Position); + sizes.Add(Config.VerstoneBar.Size); + } + + if (Config.VerfireBar.Enabled) + { + positions.Add(Config.Position + Config.VerfireBar.Position); + sizes.Add(Config.VerfireBar.Size); + } + + return (positions, sizes); + } + + public override void DrawJobHud(Vector2 origin, IPlayerCharacter player) + { + Vector2 pos = origin + Config.Position; + + if (Config.BalanceBar.Enabled) + { + DrawBalanceBar(pos, player); + } + + if (Config.WhiteManaBar.Enabled) + { + DrawWhiteManaBar(pos, player); + } + + if (Config.BlackManaBar.Enabled) + { + DrawBlackManaBar(pos, player); + } + + if (Config.ManaStacksBar.Enabled) + { + DrawManaStacksBarBar(pos, player); + } + + if (Config.DualcastBar.Enabled) + { + DrawDualCastBar(pos, player); + } + + if (Config.VerstoneBar.Enabled) + { + DrawVerstoneBar(pos, player); + } + + if (Config.VerfireBar.Enabled) + { + DrawVerfireBar(pos, player); + } + } + + private void DrawBalanceBar(Vector2 origin, IPlayerCharacter player) + { + RDMGauge gauge = Plugin.JobGauges.Get(); + float whiteGauge = (float)Plugin.JobGauges.Get().WhiteMana; + float blackGauge = (float)Plugin.JobGauges.Get().BlackMana; + int scale = gauge.WhiteMana - gauge.BlackMana; + + PluginConfigColor color = Config.BalanceBar.FillColor; + int value = 0; + + if (whiteGauge >= 50 && blackGauge >= 50) + { + value = 1; + } + else if (scale >= 30) + { + color = Config.WhiteManaBar.FillColor; + value = 1; + } + else if (scale <= -30) + { + color = Config.BlackManaBar.FillColor; + value = 1; + } + + if (Config.BalanceBar.HideWhenInactive && value == 0) + { + return; + } + + BarHud bar = BarUtilities.GetBar(Config.BalanceBar, value, 1, 0, player, color); + AddDrawActions(bar.GetDrawActions(origin, Config.BalanceBar.StrataLevel)); + } + + private void DrawWhiteManaBar(Vector2 origin, IPlayerCharacter player) + { + byte mana = Plugin.JobGauges.Get().WhiteMana; + if (Config.WhiteManaBar.HideWhenInactive && mana == 0) + { + return; + } + + Config.WhiteManaBar.Label.SetValue(mana); + + BarHud bar = BarUtilities.GetProgressBar(Config.WhiteManaBar, mana, 100, 0, player); + AddDrawActions(bar.GetDrawActions(origin, Config.WhiteManaBar.StrataLevel)); + } + + private void DrawBlackManaBar(Vector2 origin, IPlayerCharacter player) + { + byte mana = Plugin.JobGauges.Get().BlackMana; + if (Config.BlackManaBar.HideWhenInactive && mana == 0) + { + return; + } + + Config.BlackManaBar.Label.SetValue(mana); + + BarHud bar = BarUtilities.GetProgressBar(Config.BlackManaBar, mana, 100, 0, player); + AddDrawActions(bar.GetDrawActions(origin, Config.BlackManaBar.StrataLevel)); + } + + private void DrawManaStacksBarBar(Vector2 origin, IPlayerCharacter player) + { + byte manaStacks = Plugin.JobGauges.Get().ManaStacks; + if (Config.ManaStacksBar.HideWhenInactive && manaStacks == 0) + { + return; + } + + BarHud[] bars = BarUtilities.GetChunkedBars(Config.ManaStacksBar, 3, manaStacks, 3f, 0, player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.ManaStacksBar.StrataLevel)); + } + } + + private void DrawDualCastBar(Vector2 origin, IPlayerCharacter player) + { + float duration = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 1249)?.RemainingTime ?? 0f; + + if (Config.DualcastBar.HideWhenInactive && duration == 0) + { + return; + }; + + Config.DualcastBar.Label.SetValue(duration); + + BarHud bar = BarUtilities.GetProgressBar(Config.DualcastBar, duration, 15f, 0, player); + AddDrawActions(bar.GetDrawActions(origin, Config.DualcastBar.StrataLevel)); + } + + private void DrawVerstoneBar(Vector2 origin, IPlayerCharacter player) + { + BarHud? bar = BarUtilities.GetProcBar(Config.VerstoneBar, player, 1235, 30); + if (bar != null) + { + AddDrawActions(bar.GetDrawActions(origin, Config.VerstoneBar.StrataLevel)); + } + } + + private void DrawVerfireBar(Vector2 origin, IPlayerCharacter player) + { + BarHud? bar = BarUtilities.GetProcBar(Config.VerfireBar, player, 1234, 30); + if (bar != null) + { + AddDrawActions(bar.GetDrawActions(origin, Config.VerfireBar.StrataLevel)); + } + } + } + + [Section("Job Specific Bars")] + [SubSection("Caster", 0)] + [SubSection("Red Mage", 1)] + public class RedMageConfig : JobConfig + { + [JsonIgnore] public override uint JobId => JobIDs.RDM; + public new static RedMageConfig DefaultConfig() + { + var config = new RedMageConfig(); + config.UseDefaultPrimaryResourceBar = true; + + config.WhiteManaBar.ThresholdConfig.Enabled = true; + config.WhiteManaBar.ThresholdConfig.ChangeColor = false; + config.WhiteManaBar.ThresholdConfig.ShowMarker = true; + config.WhiteManaBar.Label.TextAnchor = DrawAnchor.Right; + config.WhiteManaBar.Label.FrameAnchor = DrawAnchor.Right; + config.WhiteManaBar.Label.Position = new Vector2(-2, 0); + + config.BlackManaBar.ThresholdConfig.Enabled = true; + config.BlackManaBar.ThresholdConfig.ChangeColor = false; + config.BlackManaBar.ThresholdConfig.ShowMarker = true; + config.BlackManaBar.Label.TextAnchor = DrawAnchor.Left; + config.BlackManaBar.Label.FrameAnchor = DrawAnchor.Left; + config.BlackManaBar.Label.Position = new Vector2(2, 0); + + config.DualcastBar.Label.Enabled = false; + + config.VerstoneBar.Label.FontID = FontsConfig.DefaultMediumFontKey; + config.VerstoneBar.Label.TextAnchor = DrawAnchor.Right; + config.VerstoneBar.Label.FrameAnchor = DrawAnchor.Right; + config.VerstoneBar.Label.Position = new Vector2(-2, 0); + + config.VerfireBar.Label.FontID = FontsConfig.DefaultMediumFontKey; + config.VerfireBar.Label.TextAnchor = DrawAnchor.Left; + config.VerfireBar.Label.FrameAnchor = DrawAnchor.Left; + config.VerfireBar.Label.Position = new Vector2(2, 0); + + return config; + } + + [NestedConfig("Balance Bar", 30)] + public BarConfig BalanceBar = new BarConfig( + new(0, -10), + new(20, 20), + new PluginConfigColor(new(195f / 255f, 35f / 255f, 35f / 255f, 100f / 100f)) + ); + + [NestedConfig("White Mana Bar", 35)] + public ProgressBarConfig WhiteManaBar = new ProgressBarConfig( + new(-69.5f, -10), + new(115, 20), + new PluginConfigColor(new(221f / 255f, 212f / 255f, 212f / 255f, 100f / 100f)), + BarDirection.Left, + null, + 80 + ); + + [NestedConfig("Black Mana Bar", 40)] + public ProgressBarConfig BlackManaBar = new ProgressBarConfig( + new(69.5f, -10), + new(115, 20), + new PluginConfigColor(new(60f / 255f, 81f / 255f, 197f / 255f, 100f / 100f)), + threshold: 80 + ); + + [NestedConfig("Mana Stacks Bar", 45)] + public ChunkedBarConfig ManaStacksBar = new ChunkedBarConfig( + new(0, -27), + new(254, 10), + new PluginConfigColor(new(200f / 255f, 45f / 255f, 40f / 255f, 100f / 100f)) + ); + + [NestedConfig("Dualcast Bar", 50)] + public ProgressBarConfig DualcastBar = new ProgressBarConfig( + new(0, -41), + new(16, 14), + new PluginConfigColor(new(204f / 255f, 17f / 255f, 255f / 95f, 100f / 100f)) + ); + + [NestedConfig("Verstone Ready Bar", 55)] + public ProgressBarConfig VerstoneBar = new ProgressBarConfig( + new(-68.5f, -41), + new(117, 14), + new PluginConfigColor(new(228f / 255f, 188f / 255f, 145 / 255f, 90f / 100f)), + BarDirection.Left + ); + + [NestedConfig("Verfire Ready Bar", 60)] + public ProgressBarConfig VerfireBar = new ProgressBarConfig( + new(68.5f, -41), + new(117, 14), + new PluginConfigColor(new(238f / 255f, 119f / 255f, 17 / 255f, 90f / 100f)) + ); + } +} diff --git a/Interface/Jobs/SageHud.cs b/Interface/Jobs/SageHud.cs new file mode 100644 index 0000000..b46c502 --- /dev/null +++ b/Interface/Jobs/SageHud.cs @@ -0,0 +1,246 @@ +using Dalamud.Game.ClientState.JobGauge.Types; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Objects.Types; +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using HSUI.Interface.GeneralElements; +using Newtonsoft.Json; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Dalamud.Logging; + +namespace HSUI.Interface.Jobs +{ + public class SageHud : JobHud + { + private new SageConfig Config => (SageConfig)_config; + + private static readonly List DotIDs = new() { 2614, 2615, 2616, 3897 }; + private static readonly List DotDurations = new() { 30, 30, 30, 30 }; + + public SageHud(JobConfig config, string? displayName = null) : base(config, displayName) + { + } + + protected override (List, List) ChildrenPositionsAndSizes() + { + List positions = new(); + List sizes = new(); + + if (Config.AddersgallBar.Enabled) + { + positions.Add(Config.Position + Config.AddersgallBar.Position); + sizes.Add(Config.AddersgallBar.Size); + } + + if (Config.DotBar.Enabled) + { + positions.Add(Config.Position + Config.DotBar.Position); + sizes.Add(Config.DotBar.Size); + } + + if (Config.KeracholeBar.Enabled) + { + positions.Add(Config.Position + Config.KeracholeBar.Position); + sizes.Add(Config.KeracholeBar.Size); + } + + if (Config.PhysisBar.Enabled) + { + positions.Add(Config.Position + Config.PhysisBar.Position); + sizes.Add(Config.PhysisBar.Size); + } + + return (positions, sizes); + } + + public override void DrawJobHud(Vector2 origin, IPlayerCharacter player) + { + Vector2 pos = origin + Config.Position; + + if (Config.AddersgallBar.Enabled) + { + DrawAddersgallBar(pos, player); + } + + if (Config.DotBar.Enabled) + { + DrawDotBar(pos, player); + } + + if (Config.KeracholeBar.Enabled) + { + DrawKeracholeBar(pos, player); + } + + if (Config.PhysisBar.Enabled) + { + DrawPhysisBar(pos, player); + } + } + + private void DrawDotBar(Vector2 origin, IPlayerCharacter player) + { + IGameObject? target = Plugin.TargetManager.SoftTarget ?? Plugin.TargetManager.Target; + + BarHud? bar = BarUtilities.GetDoTBar(Config.DotBar, player, target, DotIDs, DotDurations); + if (bar != null) + { + AddDrawActions(bar.GetDrawActions(origin, Config.DotBar.StrataLevel)); + } + } + + private void DrawAddersgallBar(Vector2 origin, IPlayerCharacter player) + { + SGEGauge gauge = Plugin.JobGauges.Get(); + + const float addersgallCooldown = 20000f; + + float GetScale(int num, float timer) => num + (timer / addersgallCooldown); + float adderScale = GetScale(gauge.Addersgall, gauge.AddersgallTimer); + BarGlowConfig? glow = gauge.Eukrasia && Config.EukrasiaGlow ? Config.AddersgallBar.GlowConfig : null; + + if (!Config.AddersgallBar.HideWhenInactive || adderScale > 0) + { + BarHud[] bars = BarUtilities.GetChunkedBars(Config.AddersgallBar, 3, adderScale, 3, 0, player, partialFillColor: Config.AddersgallBar.PartialFillColor, glowConfig: glow, chunksToGlow: new[] { true, true, true }); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.AddersgallBar.StrataLevel)); + } + } + + if (!Config.AdderstingBar.HideWhenInactive && Config.AdderstingBar.Enabled || gauge.Addersting > 0) + { + int adderstingStacks = player.Level > 65 ? gauge.Addersting : 0; + BarHud[] bars = BarUtilities.GetChunkedBars(Config.AdderstingBar, 3, adderstingStacks, 3, 0, player, glowConfig: glow, chunksToGlow: new[] { true, true, true }); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.AdderstingBar.StrataLevel)); + } + } + } + + private void DrawPhysisBar(Vector2 origin, IPlayerCharacter player) + { + float physisDuration = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 2617 or 2620 && o.SourceId == player.GameObjectId)?.RemainingTime ?? 0f; + + if (!Config.PhysisBar.HideWhenInactive || physisDuration > 0) + { + Config.PhysisBar.Label.SetValue(physisDuration); + BarHud bar = BarUtilities.GetProgressBar(Config.PhysisBar, physisDuration, 15f, 0f, player); + AddDrawActions(bar.GetDrawActions(origin, Config.PhysisBar.StrataLevel)); + } + } + + private void DrawKeracholeBar(Vector2 origin, IPlayerCharacter player) + { + float keracholeDuration = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 2618 or 2938 && o.SourceId == player.GameObjectId)?.RemainingTime ?? 0f; + float holosDuration = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 3003 && o.SourceId == player.GameObjectId)?.RemainingTime ?? 0f; + + if (!Config.KeracholeBar.HideWhenInactive || keracholeDuration > 0 || holosDuration > 0) + { + float duration = holosDuration > 0 ? holosDuration : keracholeDuration; + float maxDuration = holosDuration > 0 ? 20f : 15f; + + Config.KeracholeBar.Label.SetValue(duration); + BarHud bar = BarUtilities.GetProgressBar(Config.KeracholeBar, duration, maxDuration, 0f, player); + AddDrawActions(bar.GetDrawActions(origin, Config.KeracholeBar.StrataLevel)); + } + } + } + + [Section("Job Specific Bars")] + [SubSection("Healer", 0)] + [SubSection("Sage", 1)] + public class SageConfig : JobConfig + { + [JsonIgnore] public override uint JobId => JobIDs.SGE; + + public new static SageConfig DefaultConfig() + { + var config = new SageConfig(); + + config.UseDefaultPrimaryResourceBar = true; + config.DotBar.Label.FontID = FontsConfig.DefaultMediumFontKey; + + return config; + } + + [Checkbox("Enable Eukrasia Glow", spacing = true)] + [Order(30)] + public bool EukrasiaGlow = true; + + [NestedConfig("Addersgall Bar", 35)] + public AddersgallBarConfig AddersgallBar = new AddersgallBarConfig( + new(-64, -32), + new(126, 20), + new PluginConfigColor(new(197f / 255f, 247f / 255f, 255f / 255f, 100f / 100f)) + ); + + [NestedConfig("Addersting Bar", 40)] + public AdderstingBarConfig AdderstingBar = new AdderstingBarConfig( + new(64, -32), + new(126, 20), + new PluginConfigColor(new(255f / 255f, 232f / 255f, 255f / 255f, 100f / 100f)) + ); + + [NestedConfig("Eukrasian Dosis Bar", 45)] + public ProgressBarConfig DotBar = new ProgressBarConfig( + new(0, -10), + new(254, 20), + new PluginConfigColor(new(41f / 255f, 142f / 255f, 144f / 255f, 100f / 100f)) + ); + + [NestedConfig("Kerachole / Holos Bar", 50)] + public ProgressBarConfig KeracholeBar = new ProgressBarConfig( + new(64, -52), + new(126, 15), + new PluginConfigColor(new(100f / 255f, 207f / 255f, 211f / 255f, 100f / 100f)) + ); + + [NestedConfig("Physis Bar", 55)] + public ProgressBarConfig PhysisBar = new ProgressBarConfig( + new(-64, -52), + new(126, 15), + new PluginConfigColor(new(26f / 255f, 167f / 255f, 109f / 255f, 100f / 100f)) + ); + } + + [Exportable(false)] + public class AddersgallBarConfig : ChunkedBarConfig + { + [NestedConfig("Glow Color (when Eukrasia active)", 60, separator = false, spacing = true)] + public BarGlowConfig GlowConfig = new(); + + [Checkbox("Use Partial Fill Color", spacing = true)] + [Order(65)] + public bool UsePartialFillColor = false; + + [ColorEdit4("Partial Fill Color")] + [Order(66, collapseWith = nameof(UsePartialFillColor))] + public PluginConfigColor PartialFillColor; + + public AddersgallBarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) + : base(position, size, fillColor) + { + GlowConfig.Color = new PluginConfigColor(new(247f / 255f, 177f / 255f, 67f / 255f, 100f / 100f)); + PartialFillColor = new PluginConfigColor(new(197 / 255f, 247f / 255f, 255f / 255f, 50f / 100f)); + } + } + + [Exportable(false)] + public class AdderstingBarConfig : ChunkedBarConfig + { + [NestedConfig("Glow Color (when Eukrasia active)", 60, separator = false, spacing = true)] + public BarGlowConfig GlowConfig = new(); + + public AdderstingBarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) + : base(position, size, fillColor) + { + GlowConfig.Color = new PluginConfigColor(new(247f / 255f, 177f / 255f, 67f / 255f, 100f / 100f)); + } + } +} diff --git a/Interface/Jobs/SamuraiHud.cs b/Interface/Jobs/SamuraiHud.cs new file mode 100644 index 0000000..d089cd1 --- /dev/null +++ b/Interface/Jobs/SamuraiHud.cs @@ -0,0 +1,274 @@ +using Dalamud.Game.ClientState.JobGauge.Types; +using Dalamud.Game.ClientState.Objects.SubKinds; +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using HSUI.Interface.GeneralElements; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace HSUI.Interface.Jobs +{ + public class SamuraiHud : JobHud + { + private new SamuraiConfig Config => (SamuraiConfig)_config; + private static readonly List HiganbanaIDs = new() { 1228, 1319 }; + private static readonly List HiganabaDurations = new() { 60f, 60f }; + + public SamuraiHud(SamuraiConfig config, string? displayName = null) : base(config, displayName) + { + } + + protected override (List, List) ChildrenPositionsAndSizes() + { + List positions = new List(); + List sizes = new List(); + + if (Config.KenkiBar.Enabled) + { + positions.Add(Config.Position + Config.KenkiBar.Position); + sizes.Add(Config.KenkiBar.Size); + } + + if (Config.ShifuBar.Enabled) + { + positions.Add(Config.Position + Config.ShifuBar.Position); + sizes.Add(Config.ShifuBar.Size); + } + + if (Config.JinpuBar.Enabled) + { + positions.Add(Config.Position + Config.JinpuBar.Position); + sizes.Add(Config.JinpuBar.Size); + } + + if (Config.HiganbanaBar.Enabled) + { + positions.Add(Config.Position + Config.HiganbanaBar.Position); + sizes.Add(Config.HiganbanaBar.Size); + } + + if (Config.SenBar.Enabled) + { + positions.Add(Config.Position + Config.SenBar.Position); + sizes.Add(Config.SenBar.Size); + } + + if (Config.MeditationBar.Enabled) + { + positions.Add(Config.Position + Config.MeditationBar.Position); + sizes.Add(Config.MeditationBar.Size); + } + + return (positions, sizes); + } + + public override void DrawJobHud(Vector2 origin, IPlayerCharacter player) + { + if (Config.KenkiBar.Enabled) + { + DrawKenkiBar(origin + Config.Position, player); + } + + if (Config.ShifuBar.Enabled) + { + DrawShifuBar(origin + Config.Position, player); + } + + if (Config.JinpuBar.Enabled) + { + DrawJinpuBar(origin + Config.Position, player); + } + + if (Config.SenBar.Enabled) + { + DrawSenBar(origin + Config.Position, player); + } + + if (Config.MeditationBar.Enabled) + { + DrawMeditationBar(origin + Config.Position, player); + } + + if (Config.HiganbanaBar.Enabled) + { + DrawHiganbanaBar(origin + Config.Position, player); + } + } + + private void DrawKenkiBar(Vector2 origin, IPlayerCharacter player) + { + SAMGauge gauge = Plugin.JobGauges.Get(); + + if (!Config.KenkiBar.HideWhenInactive || gauge.Kenki > 0) + { + Config.KenkiBar.Label.SetValue(gauge.Kenki); + + BarHud bar = BarUtilities.GetProgressBar(Config.KenkiBar, gauge.Kenki, 100f, 0f, player); + AddDrawActions(bar.GetDrawActions(origin, Config.KenkiBar.StrataLevel)); + } + } + + private void DrawShifuBar(Vector2 origin, IPlayerCharacter player) + { + float shifuDuration = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 1299)?.RemainingTime ?? 0f; + + if (!Config.ShifuBar.HideWhenInactive || shifuDuration > 0) + { + Config.ShifuBar.Label.SetValue(shifuDuration); + + BarHud bar = BarUtilities.GetProgressBar(Config.ShifuBar, shifuDuration, 40f, 0f, player); + AddDrawActions(bar.GetDrawActions(origin, Config.ShifuBar.StrataLevel)); + } + } + + private void DrawJinpuBar(Vector2 origin, IPlayerCharacter player) + { + float jinpuDuration = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 1298)?.RemainingTime ?? 0f; + + if (!Config.JinpuBar.HideWhenInactive || jinpuDuration > 0) + { + Config.JinpuBar.Label.SetValue(jinpuDuration); + + BarHud bar = BarUtilities.GetProgressBar(Config.JinpuBar, jinpuDuration, 40f, 0f, player); + AddDrawActions(bar.GetDrawActions(origin, Config.JinpuBar.StrataLevel)); + } + } + + private void DrawHiganbanaBar(Vector2 origin, IPlayerCharacter player) + { + var target = Plugin.TargetManager.SoftTarget ?? Plugin.TargetManager.Target; + + BarHud? bar = BarUtilities.GetDoTBar(Config.HiganbanaBar, player, target, HiganbanaIDs, HiganabaDurations); + if (bar != null) + { + AddDrawActions(bar.GetDrawActions(origin, Config.HiganbanaBar.StrataLevel)); + } + } + + private void DrawSenBar(Vector2 origin, IPlayerCharacter player) + { + SAMGauge gauge = Plugin.JobGauges.Get(); + if (!Config.SenBar.HideWhenInactive || gauge.HasSetsu || gauge.HasGetsu || gauge.HasKa) + { + var order = Config.SenBar.SenOrder; + var hasSen = new[] { gauge.HasSetsu ? 1 : 0, gauge.HasGetsu ? 1 : 0, gauge.HasKa ? 1 : 0 }; + var colors = new[] { Config.SenBar.SetsuColor, Config.SenBar.GetsuColor, Config.SenBar.KaColor }; + + var sen = new Tuple[3]; + for (int i = 0; i < 3; i++) + { + sen[i] = new Tuple(colors[order[i]], hasSen[order[i]], null); + } + + BarHud[] bars = BarUtilities.GetChunkedBars(Config.SenBar, sen, player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.SenBar.StrataLevel)); + } + } + } + + private void DrawMeditationBar(Vector2 origin, IPlayerCharacter player) + { + SAMGauge gauge = Plugin.JobGauges.Get(); + if (!Config.MeditationBar.HideWhenInactive || gauge.MeditationStacks > 0) + { + BarHud[] bars = BarUtilities.GetChunkedBars(Config.MeditationBar, 3, gauge.MeditationStacks, 3f, 0, player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.MeditationBar.StrataLevel)); + } + } + } + } + + [Section("Job Specific Bars")] + [SubSection("Melee", 0)] + [SubSection("Samurai", 1)] + public class SamuraiConfig : JobConfig + { + [JsonIgnore] public override uint JobId => JobIDs.SAM; + + public new static SamuraiConfig DefaultConfig() + { + var config = new SamuraiConfig(); + + config.HiganbanaBar.ThresholdConfig.Enabled = true; + + return config; + } + + [NestedConfig("Sen Bar", 40)] + public SamuraiSenBarConfig SenBar = new SamuraiSenBarConfig( + new(0, -17), + new(254, 10), + new PluginConfigColor(new Vector4(0, 0, 0, 0)) + ); + + [NestedConfig("Fuka Bar", 45)] + public ProgressBarConfig ShifuBar = new ProgressBarConfig( + new(-64, -56), + new(126, 20), + new PluginConfigColor(new(219f / 255f, 211f / 255f, 136f / 255f, 100f / 100f)) + ); + + [NestedConfig("Fugetsu Bar", 50)] + public ProgressBarConfig JinpuBar = new ProgressBarConfig( + new(64, -56), + new(126, 20), + new PluginConfigColor(new(136f / 255f, 146f / 255f, 219f / 255f, 100f / 100f)) + ); + + [NestedConfig("Kenki Bar", 55)] + public ProgressBarConfig KenkiBar = new ProgressBarConfig( + new(0, -34), + new(254, 20), + new PluginConfigColor(new(255f / 255f, 82f / 255f, 82f / 255f, 53f / 100f)) + ); + + [NestedConfig("Higanbana Bar", 60)] + public ProgressBarConfig HiganbanaBar = new ProgressBarConfig( + new(0, -78), + new(254, 20), + new PluginConfigColor(new(237f / 255f, 141f / 255f, 7f / 255f, 100f / 100f)), + BarDirection.Right, + new PluginConfigColor(new(230f / 255f, 33f / 255f, 33f / 255f, 53f / 100f)), + 15f + ); + + [NestedConfig("Meditation Bar", 65)] + public ChunkedBarConfig MeditationBar = new ChunkedBarConfig( + new(0, -5), + new(254, 10), + new PluginConfigColor(new(247f / 255f, 163f / 255f, 89f / 255f, 100f / 100f)) + ); + } + + [DisableParentSettings("FillColor")] + [Exportable(false)] + public class SamuraiSenBarConfig : ChunkedBarConfig + { + [ColorEdit4("Setsu", spacing = true)] + [Order(60)] + public PluginConfigColor SetsuColor = new PluginConfigColor(new(89f / 255f, 234f / 255f, 247f / 255f, 100f / 100f)); + + [ColorEdit4("Getsu")] + [Order(65)] + public PluginConfigColor GetsuColor = new PluginConfigColor(new(89f / 255f, 126f / 255f, 247f / 255f, 100f / 100f)); + + [ColorEdit4("Ka")] + [Order(70)] + public PluginConfigColor KaColor = new PluginConfigColor(new(247f / 255f, 89f / 255f, 89f / 255f, 100f / 100f)); + + [DragDropHorizontal("Order", "Setsu", "Getsu", "Ka")] + [Order(75)] + public int[] SenOrder = new int[] { 0, 1, 2 }; + + public SamuraiSenBarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) : base(position, size, fillColor, 2) { } + } +} diff --git a/Interface/Jobs/ScholarHud.cs b/Interface/Jobs/ScholarHud.cs new file mode 100644 index 0000000..023fc11 --- /dev/null +++ b/Interface/Jobs/ScholarHud.cs @@ -0,0 +1,209 @@ +using Dalamud.Game.ClientState.JobGauge.Types; +using Dalamud.Game.ClientState.Objects.SubKinds; +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace HSUI.Interface.Jobs +{ + public class ScholarHud : JobHud + { + private new ScholarConfig Config => (ScholarConfig)_config; + + private static readonly List BioDoTIDs = new() { 179, 189, 1895 }; + private static readonly List BioDoTDurations = new() { 30, 30, 30 }; + + public ScholarHud(ScholarConfig config, string? displayName = null) : base(config, displayName) + { + } + + protected override (List, List) ChildrenPositionsAndSizes() + { + List positions = new List(); + List sizes = new List(); + + if (Config.AetherflowBar.Enabled) + { + positions.Add(Config.Position + Config.AetherflowBar.Position); + sizes.Add(Config.AetherflowBar.Size); + } + + if (Config.FairyGaugeBar.Enabled) + { + positions.Add(Config.Position + Config.FairyGaugeBar.Position); + sizes.Add(Config.FairyGaugeBar.Size); + } + + if (Config.BioBar.Enabled) + { + positions.Add(Config.Position + Config.BioBar.Position); + sizes.Add(Config.BioBar.Size); + } + + if (Config.SacredSoilBar.Enabled) + { + positions.Add(Config.Position + Config.SacredSoilBar.Position); + sizes.Add(Config.SacredSoilBar.Size); + } + + return (positions, sizes); + } + + public override void DrawJobHud(Vector2 origin, IPlayerCharacter player) + { + Vector2 pos = origin + Config.Position; + + if (Config.BioBar.Enabled) + { + DrawBioBar(pos, player); + } + + if (Config.FairyGaugeBar.Enabled) + { + DrawFairyGaugeBar(pos, player); + } + + if (Config.AetherflowBar.Enabled) + { + DrawAetherBar(pos, player); + } + + if (Config.SacredSoilBar.Enabled) + { + DrawSacredSoilBar(pos, player); + } + } + + private void DrawBioBar(Vector2 origin, IPlayerCharacter player) + { + var target = Plugin.TargetManager.SoftTarget ?? Plugin.TargetManager.Target; + + BarHud? bar = BarUtilities.GetDoTBar(Config.BioBar, player, target, BioDoTIDs, BioDoTDurations); + if (bar != null) + { + AddDrawActions(bar.GetDrawActions(origin, Config.BioBar.StrataLevel)); + } + } + + private unsafe void DrawFairyGaugeBar(Vector2 origin, IPlayerCharacter player) + { + SCHGauge gauge = Plugin.JobGauges.Get(); + byte fairyGauge = gauge.FairyGauge; + float seraphDuration = gauge.SeraphTimer; + + if (Config.FairyGaugeBar.HideWhenInactive && fairyGauge == 0 && (seraphDuration == 0 || !Config.FairyGaugeBar.ShowSeraph)) + { + return; + } + + if (Config.FairyGaugeBar.ShowSeraph && seraphDuration > 0) + { + Config.FairyGaugeBar.Label.SetValue(seraphDuration / 1000); + + BarHud bar = BarUtilities.GetProgressBar(Config.FairyGaugeBar, seraphDuration / 1000, 22, 0, player, Config.FairyGaugeBar.SeraphColor); + AddDrawActions(bar.GetDrawActions(origin, Config.FairyGaugeBar.StrataLevel)); + } + else + { + Config.FairyGaugeBar.Label.SetValue(fairyGauge); + + BarHud bar = BarUtilities.GetProgressBar(Config.FairyGaugeBar, fairyGauge, 100, 0, player); + AddDrawActions(bar.GetDrawActions(origin, Config.FairyGaugeBar.StrataLevel)); + } + } + + private void DrawAetherBar(Vector2 origin, IPlayerCharacter player) + { + ushort stackCount = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 304)?.Param ?? 0; + + if (Config.AetherflowBar.HideWhenInactive && stackCount == 0) + { + return; + }; + + BarHud[] bars = BarUtilities.GetChunkedBars(Config.AetherflowBar, 3, stackCount, 3, 0, player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.AetherflowBar.StrataLevel)); + } + } + + private void DrawSacredSoilBar(Vector2 origin, IPlayerCharacter player) + { + float sacredSoilDuration = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 298 or 1944 && o.SourceId == player.GameObjectId)?.RemainingTime ?? 0f; + + if (!Config.SacredSoilBar.HideWhenInactive || sacredSoilDuration > 0) + { + Config.SacredSoilBar.Label.SetValue(sacredSoilDuration); + + BarHud bar = BarUtilities.GetProgressBar(Config.SacredSoilBar, sacredSoilDuration, 15f, 0f, player); + AddDrawActions(bar.GetDrawActions(origin, Config.SacredSoilBar.StrataLevel)); + } + } + } + + [Section("Job Specific Bars")] + [SubSection("Healer", 0)] + [SubSection("Scholar", 1)] + public class ScholarConfig : JobConfig + { + [JsonIgnore] public override uint JobId => JobIDs.SCH; + public new static ScholarConfig DefaultConfig() + { + var config = new ScholarConfig(); + config.UseDefaultPrimaryResourceBar = true; + return config; + } + + [NestedConfig("Bio Bar", 30)] + public ProgressBarConfig BioBar = new ProgressBarConfig( + new(0, -10), + new(254, 20), + new(new Vector4(50f / 255f, 93f / 255f, 37f / 255f, 1f)) + ); + + [NestedConfig("Fairy Gauge", 35)] + public ScholarFairyGaugeBarConfig FairyGaugeBar = new ScholarFairyGaugeBarConfig( + new(0, -32), + new(254, 20), + new(new Vector4(69f / 255f, 199 / 255f, 164f / 255f, 100f / 100f)) + ); + + [NestedConfig("Aetherflow Bar", 40)] + public ChunkedBarConfig AetherflowBar = new ChunkedBarConfig( + new(0, -54), + new(254, 20), + new(new Vector4(0f / 255f, 255f / 255f, 0f / 255f, 100f / 100f)) + ); + + [NestedConfig("Sacred Soil Bar", 45)] + public ProgressBarConfig SacredSoilBar = new ProgressBarConfig( + new(-0, -76), + new(254, 20), + new PluginConfigColor(new(241f / 255f, 217f / 255f, 125f / 255f, 100f / 100f)) + ); + } + + [Exportable(false)] + public class ScholarFairyGaugeBarConfig : ProgressBarConfig + { + [Checkbox("Show Seraph", spacing = true)] + [Order(50)] + public bool ShowSeraph = true; + + [ColorEdit4("Color" + "##SeraphColor")] + [Order(55)] + public PluginConfigColor SeraphColor = new(new Vector4(232f / 255f, 255f / 255f, 255f / 255f, 100f / 100f)); + + public ScholarFairyGaugeBarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) + : base(position, size, fillColor) + { + } + } +} diff --git a/Interface/Jobs/SummonerHud.cs b/Interface/Jobs/SummonerHud.cs new file mode 100644 index 0000000..e0d25da --- /dev/null +++ b/Interface/Jobs/SummonerHud.cs @@ -0,0 +1,429 @@ +using Dalamud.Game.ClientState.JobGauge.Types; +using Dalamud.Game.ClientState.Objects.SubKinds; +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using HSUI.Interface.GeneralElements; +using FFXIVClientStructs.FFXIV.Client.Game.Gauge; +using FFXIVClientStructs.FFXIV.Common.Lua; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace HSUI.Interface.Jobs +{ + public class SummonerHud : JobHud + { + private new SummonerConfig Config => (SummonerConfig)_config; + + public SummonerHud(SummonerConfig config, string? displayName = null) : base(config, displayName) + { + } + + protected override (List, List) ChildrenPositionsAndSizes() + { + List positions = new(); + List sizes = new(); + + if (Config.AetherflowBar.Enabled) + { + positions.Add(Config.Position + Config.AetherflowBar.Position); + sizes.Add(Config.AetherflowBar.Size); + } + + if (Config.TranceBar.Enabled) + { + positions.Add(Config.Position + Config.TranceBar.Position); + sizes.Add(Config.TranceBar.Size); + } + + if (Config.IfritBar.Enabled) + { + positions.Add(Config.Position + Config.IfritBar.Position); + sizes.Add(Config.IfritBar.Size); + } + + if (Config.TitanBar.Enabled) + { + positions.Add(Config.Position + Config.TitanBar.Position); + sizes.Add(Config.TitanBar.Size); + } + + if (Config.GarudaBar.Enabled) + { + positions.Add(Config.Position + Config.GarudaBar.Position); + sizes.Add(Config.GarudaBar.Size); + } + + if (Config.StacksBar.Enabled) + { + positions.Add(Config.Position + Config.StacksBar.Position); + sizes.Add(Config.StacksBar.Size); + } + + return (positions, sizes); + } + + public override void DrawJobHud(Vector2 origin, IPlayerCharacter player) + { + Vector2 pos = origin + Config.Position; + + if (Config.AetherflowBar.Enabled) + { + DrawAetherBar(pos, player); + } + + if (Config.TranceBar.Enabled) + { + DrawTranceBar(pos, player); + } + + if (Config.IfritBar.Enabled) + { + DrawIfritBar(pos, player); + } + + if (Config.TitanBar.Enabled) + { + DrawTitanBar(pos, player); + } + + if (Config.GarudaBar.Enabled) + { + DrawGarudaBar(pos, player); + } + + if (Config.StacksBar.Enabled) + { + HandleAttunementStacks(pos, player); + } + } + + private unsafe void DrawIfritBar(Vector2 origin, IPlayerCharacter player) + { + SMNGauge gauge = Plugin.JobGauges.Get(); + int stackCount = gauge.IsIfritReady ? 1 : 0; + + if (!Config.IfritBar.HideWhenInactive || stackCount > 1) + { + BarHud[] bars = BarUtilities.GetChunkedBars(Config.IfritBar, 1, stackCount, 1, 0, player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.IfritBar.StrataLevel)); + } + } + } + + private void DrawTitanBar(Vector2 origin, IPlayerCharacter player) + { + SMNGauge gauge = Plugin.JobGauges.Get(); + int stackCount = gauge.IsTitanReady ? 1 : 0; + + if (!Config.TitanBar.HideWhenInactive || stackCount > 1) + { + BarHud[] bars = BarUtilities.GetChunkedBars(Config.TitanBar, 1, stackCount, 1, 0, player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.TitanBar.StrataLevel)); + } + } + } + + private void DrawGarudaBar(Vector2 origin, IPlayerCharacter player) + { + SMNGauge gauge = Plugin.JobGauges.Get(); + int stackCount = gauge.IsGarudaReady ? 1 : 0; + + if (!Config.GarudaBar.HideWhenInactive || stackCount > 1) + { + BarHud[] bars = BarUtilities.GetChunkedBars(Config.GarudaBar, 1, stackCount, 1, 0, player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.GarudaBar.StrataLevel)); + } + } + } + + private enum Primal + { + None = 0, + Ifrit = 1, + Titan = 2, + Garuda = 3 + } + + private unsafe void HandleAttunementStacks(Vector2 origin, IPlayerCharacter player) + { + SMNGauge gauge = Plugin.JobGauges.Get(); + + byte value = *((byte*)(new IntPtr(gauge.Address) + 0xE)); + Primal primal = (Primal)(value & 3); + int stacks = ((value >> 2) & 7); + + if (primal == Primal.Ifrit && Config.StacksBar.ShowIfritStacks) + { + DrawStacksBar(origin, player, stacks, 2, Config.StacksBar.IfritStackColor); + } + else if (primal == Primal.Titan && Config.StacksBar.ShowTitanStacks) + { + DrawStacksBar(origin, player, stacks, 4, Config.StacksBar.TitanStackColor); + } + else if (primal == Primal.Garuda && Config.StacksBar.ShowGarudaStacks) + { + DrawStacksBar(origin, player, stacks, 4, Config.StacksBar.GarudaStackColor); + } + else if (!Config.StacksBar.HideWhenInactive) + { + DrawStacksBar(origin, player, 0, 1, Config.StacksBar.FillColor); + } + } + + private void DrawAetherBar(Vector2 origin, IPlayerCharacter player) + { + ushort stackCount = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId == 304)?.Param ?? 0; + + if (Config.AetherflowBar.HideWhenInactive && stackCount == 0) + { + return; + } + + BarHud[] bars = BarUtilities.GetChunkedBars(Config.AetherflowBar, 2, stackCount, 2, 0, player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.AetherflowBar.StrataLevel)); + } + } + + private unsafe void DrawTranceBar(Vector2 origin, IPlayerCharacter player) + { + SMNGauge gauge = Plugin.JobGauges.Get(); + PluginConfigColor tranceColor; + uint spellID = 0; + float maxDuration = 0f; + float currentCooldown = 0f; + float tranceDuration = 0f; + tranceColor = Config.TranceBar.FillColor; + + // Dawntrail Fixes + bool isSolarBahamutReady = gauge.AetherFlags.HasFlag(AetherFlags.None + 0x8) || // 0x8 Formerly Titan Attuned + gauge.AetherFlags.HasFlag(AetherFlags.None + 0xC); // 0xC Formerly Garuda Attuned + bool isPhoenixReady = gauge.AetherFlags.HasFlag(AetherFlags.None + 0x4); // 0x4 Formerly Ifrit Attuned + bool isNormalBahamutReady = !isSolarBahamutReady && !isPhoenixReady; // You'd think it would be 0x10, but thats unused now + + byte summonedPrimal = *((byte*)(new IntPtr(gauge.Address) + 0xE)); // Formally Attunement, now...? + Primal primal = (Primal)(summonedPrimal & 3); + + if (primal != Primal.None) + { + tranceColor = primal == Primal.Ifrit ? Config.TranceBar.IfritColor : primal == Primal.Titan ? Config.TranceBar.TitanColor : primal == Primal.Garuda ? Config.TranceBar.GarudaColor : Config.TranceBar.FillColor; + tranceDuration = gauge.AttunementTimerRemaining; + maxDuration = 30f; + } + else + { + if (isSolarBahamutReady) + { + tranceColor = Config.TranceBar.SolarBahamutColor; + tranceDuration = gauge.SummonTimerRemaining; + spellID = 36992; + maxDuration = 15f; + } + else if (isNormalBahamutReady) + { + tranceColor = Config.TranceBar.BahamutColor; + tranceDuration = gauge.SummonTimerRemaining; + spellID = 7427; + maxDuration = 15f; + } + else if (isPhoenixReady) + { + tranceColor = Config.TranceBar.PhoenixColor; + tranceDuration = gauge.SummonTimerRemaining; + spellID = 25831; + maxDuration = 15f; + } + } + + if (tranceDuration != 0) + { + if (gauge.AttunementTimerRemaining > 0 && Config.TranceBar.HidePrimals) + { + return; + } + + Config.TranceBar.Label.SetValue(tranceDuration / 1000f); + + BarHud bar = BarUtilities.GetProgressBar(Config.TranceBar, tranceDuration / 1000f, maxDuration, 0, player, tranceColor); + AddDrawActions(bar.GetDrawActions(origin, Config.TranceBar.StrataLevel)); + } + else + { + if (!Config.TranceBar.HideWhenInactive) + { + if (gauge.AttunementTimerRemaining == 0) + { + maxDuration = SpellHelper.Instance.GetRecastTime(spellID); + float tranceCooldown = SpellHelper.Instance.GetSpellCooldown(spellID); + currentCooldown = maxDuration - tranceCooldown; + + Config.TranceBar.Label.SetValue(maxDuration - currentCooldown); + if (currentCooldown == maxDuration) + { + Config.TranceBar.Label.SetText("READY"); + } + + BarHud bar = BarUtilities.GetProgressBar(Config.TranceBar, currentCooldown, maxDuration, 0, player, tranceColor); + AddDrawActions(bar.GetDrawActions(origin, Config.TranceBar.StrataLevel)); + } + } + } + } + + private void DrawStacksBar(Vector2 origin, IPlayerCharacter player, int amount, int max, PluginConfigColor stackColor, BarGlowConfig? glowConfig = null) + { + SummonerStacksBarConfig config = Config.StacksBar; + + config.FillColor = stackColor; + + BarHud[] bars = BarUtilities.GetChunkedBars(Config.StacksBar, max, amount, max, 0f, player, glowConfig: glowConfig); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.StacksBar.StrataLevel)); + } + } + } + + [Section("Job Specific Bars")] + [SubSection("Caster", 0)] + [SubSection("Summoner", 1)] + public class SummonerConfig : JobConfig + { + [JsonIgnore] public override uint JobId => JobIDs.SMN; + public new static SummonerConfig DefaultConfig() + { + var config = new SummonerConfig(); + + config.TranceBar.Label.FontID = FontsConfig.DefaultMediumFontKey; + config.UseDefaultPrimaryResourceBar = true; + + return config; + } + + [NestedConfig("Aetherflow Bar", 40)] + public ChunkedBarConfig AetherflowBar = new ChunkedBarConfig( + new(-0, -7), + new(254, 14), + new(new Vector4(255f / 255f, 177f / 255f, 255f / 255f, 100f / 100f)) + ); + + [NestedConfig("Trance Bar", 45)] + public SummonerTranceBarConfig TranceBar = new SummonerTranceBarConfig( + new(0, -23), + new(254, 14), + new(new Vector4(128f / 255f, 255f / 255f, 255f / 255f, 100f / 100f)) + ); + + [NestedConfig("Ifrit Bar", 50)] + public ChunkedBarConfig IfritBar = new ChunkedBarConfig( + new(-85, -39), + new(84, 14), + new(new Vector4(200f / 255f, 40f / 255f, 0f / 255f, 100f / 100f)) + ); + + [NestedConfig("Titan Bar", 55)] + public ChunkedBarConfig TitanBar = new ChunkedBarConfig( + new(0, -39), + new(84, 14), + new(new Vector4(210f / 255f, 150f / 255f, 26f / 255f, 100f / 100f)) + ); + + [NestedConfig("Garuda Bar", 60)] + public ChunkedBarConfig GarudaBar = new ChunkedBarConfig( + new(85, -39), + new(84, 14), + new(new Vector4(60f / 255f, 160f / 255f, 100f / 255f, 100f / 100f)) + ); + + [NestedConfig("Attunement Stacks Bar", 65)] + public SummonerStacksBarConfig StacksBar = new SummonerStacksBarConfig( + new(0, -55), + new(254, 14), + new PluginConfigColor(new Vector4(255f / 255f, 255f / 255f, 255f / 255f, 0f / 100f)) + ); + + } + + + [Exportable(false)] + public class SummonerTranceBarConfig : ProgressBarConfig + { + [ColorEdit4("Bahamut Color")] + [Order(26)] + public PluginConfigColor BahamutColor = new(new Vector4(128f / 255f, 255f / 255f, 255f / 255f, 100f / 100f)); + + [ColorEdit4("Phoenix Color")] + [Order(27)] + public PluginConfigColor PhoenixColor = new(new Vector4(240f / 255f, 100f / 255f, 10f / 255f, 100f / 100f)); + + [ColorEdit4("Solar Bahamut Color")] + [Order(28)] + public PluginConfigColor SolarBahamutColor = new(new Vector4(235f / 255f, 241f / 255f, 252f / 255f, 100f / 100f)); + + [ColorEdit4("Ifrit Color")] + [Order(29)] + public PluginConfigColor IfritColor = new(new Vector4(200f / 255f, 40f / 255f, 0f / 255f, 100f / 100f)); + + [ColorEdit4("Titan Color")] + [Order(30)] + public PluginConfigColor TitanColor = new(new Vector4(210f / 255f, 150f / 255f, 26f / 255f, 100f / 100f)); + + [ColorEdit4("Garuda Color")] + [Order(31)] + public PluginConfigColor GarudaColor = new(new Vector4(60f / 255f, 160f / 255f, 100f / 255f, 100f / 100f)); + + [Checkbox("Hide Primals")] + [Order(45)] + public bool HidePrimals = false; + + public SummonerTranceBarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) + : base(position, size, fillColor) + { + } + } + + [Exportable(false)] + public class SummonerStacksBarConfig : ChunkedBarConfig + { + [Checkbox("Ifrit Stacks", separator = false, spacing = false)] + [Order(51)] + public bool ShowIfritStacks = true; + + [Checkbox("Titan Stacks", separator = false, spacing = false)] + [Order(53)] + public bool ShowTitanStacks = true; + + [Checkbox("Garuda Stacks", separator = false, spacing = false)] + [Order(55)] + public bool ShowGarudaStacks = true; + + [ColorEdit4("Ifrit Stacks Color")] + [Order(56)] + public PluginConfigColor IfritStackColor = new(new Vector4(200f / 255f, 40f / 255f, 0f / 255f, 100f / 100f)); + + [ColorEdit4("Titan Stacks Color")] + [Order(57)] + public PluginConfigColor TitanStackColor = new(new Vector4(210f / 255f, 150f / 255f, 26f / 255f, 100f / 100f)); + + [ColorEdit4("Garuda Stacks Color")] + [Order(58)] + public PluginConfigColor GarudaStackColor = new(new Vector4(60f / 255f, 160f / 255f, 100f / 255f, 100f / 100f)); + + public SummonerStacksBarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) + : base(position, size, fillColor) + { + } + } +} diff --git a/Interface/Jobs/ViperHud.cs b/Interface/Jobs/ViperHud.cs new file mode 100644 index 0000000..86951d3 --- /dev/null +++ b/Interface/Jobs/ViperHud.cs @@ -0,0 +1,373 @@ +using Dalamud.Game.ClientState.JobGauge.Types; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Objects.Types; +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using HSUI.Interface.GeneralElements; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace HSUI.Interface.Jobs +{ + public class ViperHud : JobHud + { + private new ViperConfig Config => (ViperConfig)_config; + + public ViperHud(ViperConfig config, string? displayName = null) : base(config, displayName) { } + + protected override (List, List) ChildrenPositionsAndSizes() + { + List positions = new List(); + List sizes = new List(); + + if (Config.RattlingCoilGauge.Enabled) + { + positions.Add(Config.Position + Config.RattlingCoilGauge.Position); + sizes.Add(Config.RattlingCoilGauge.Size); + } + + if (Config.Vipersight.Enabled) + { + positions.Add(Config.Position + Config.Vipersight.Position); + sizes.Add(Config.Vipersight.Size); + } + + if (Config.AnguineTribute.Enabled) + { + positions.Add(Config.Position + Config.AnguineTribute.Position); + sizes.Add(Config.AnguineTribute.Size); + } + + if (Config.SerpentOfferings.Enabled) + { + positions.Add(Config.Position + Config.SerpentOfferings.Position); + sizes.Add(Config.SerpentOfferings.Size); + } + + return (positions, sizes); + } + + public override void DrawJobHud(Vector2 origin, IPlayerCharacter player) + { + if (Config.RattlingCoilGauge.Enabled) + { + DrawRattlingCoilGauge(origin + Config.Position, player); + } + + if (Config.Vipersight.Enabled) + { + DrawVipersightBar(origin + Config.Position, player); + } + + if (Config.SerpentOfferings.Enabled) + { + DrawSerpentOfferingsBar(origin + Config.Position, player); + } + + if (Config.AnguineTribute.Enabled) + { + DrawAnguineTributeGauge(origin + Config.Position, player); + } + } + + private unsafe void DrawVipersightBar(Vector2 origin, IPlayerCharacter player) + { + ViperCombo lastUsedActionId = (ViperCombo)SpellHelper.Instance.GetLastUsedActionId(); + ViperComboState comboState; + bool isAoE = false; + + switch (lastUsedActionId) + { + case ViperCombo.SteelMaw: + case ViperCombo.DreadMaw: + isAoE = true; + comboState = ViperComboState.Started; + break; + case ViperCombo.SteelFangs: + case ViperCombo.DreadFangs: + comboState = ViperComboState.Started; + break; + case ViperCombo.HuntersBite: + case ViperCombo.SwiftskinsBite: + isAoE = true; + comboState = ViperComboState.Finisher; + break; + case ViperCombo.HuntersSting: + case ViperCombo.SwiftskinsSting: + comboState = ViperComboState.Finisher; + break; + default: + comboState = ViperComboState.None; + break; + } + + if (Config.Vipersight.HideWhenInactive && comboState == ViperComboState.None) + { + return; + } + + uint leftId = SpellHelper.Instance.GetSpellActionId(isAoE ? (uint)ViperCombo.SteelMaw : (uint)ViperCombo.SteelFangs); + bool isLeftGlowing = SpellHelper.Instance.IsActionHighlighted(leftId); + + uint rightId = SpellHelper.Instance.GetSpellActionId(isAoE ? (uint)ViperCombo.DreadMaw : (uint)ViperCombo.DreadFangs); + bool isRightGlowing = SpellHelper.Instance.IsActionHighlighted(rightId); + + List> chunks = new(); + List glows = new(); + + Tuple empty = new(PluginConfigColor.Empty, 1, null); + Tuple start = new(Config.Vipersight.ComboStartColor, 1, null); + Tuple endFlank = new(Config.Vipersight.ComboEndFlankColor, 1, null); + Tuple endHind = new(Config.Vipersight.ComboEndHindColor, 1, null); + Tuple endAoE = new(Config.Vipersight.ComboEndAOEColor, 1, null); + + bool isFlankEnder = Utils.StatusListForBattleChara(player).Any(o => o.StatusId is 3645 or 3646); + bool isHindEnder = Utils.StatusListForBattleChara(player).Any(o => o.StatusId is 3647 or 3648); + bool noEnder = !isFlankEnder && !isHindEnder; + + switch (comboState) + { + case ViperComboState.None: + { + chunks = [empty, empty, empty, empty]; + glows = [false, isLeftGlowing, isRightGlowing, false]; + break; + } + case ViperComboState.Started: + { + chunks = [empty, start, start, empty]; + glows = [false, isLeftGlowing || isAoE, isRightGlowing || isAoE, false]; + break; + } + case ViperComboState.Finisher: + { + bool isFlankChain = lastUsedActionId == ViperCombo.HuntersSting; + bool isHindChain = lastUsedActionId == ViperCombo.SwiftskinsSting; + + Tuple end; + + if (isFlankEnder) + { + end = isHindChain ? endHind : endFlank; + } + else if (isHindEnder) + { + end = isFlankChain ? endFlank : endHind; + } + else + { + end = isFlankChain ? endFlank : isHindChain ? endHind : endAoE; + } + + chunks = [end, start, start, end]; + glows = [isLeftGlowing, isLeftGlowing, isRightGlowing, isRightGlowing]; + + break; + } + } + + if (Config.Vipersight.Invert) + { + chunks.Reverse(); + glows.Reverse(); + } + + BarHud[] bars = BarUtilities.GetChunkedBars( + Config.Vipersight, + chunks.ToArray(), + player, + Config.Vipersight.GlowConfig, + glows.ToArray() + ); + + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.Vipersight.StrataLevel)); + } + } + + private unsafe void DrawRattlingCoilGauge(Vector2 origin, IPlayerCharacter player) + { + VPRGauge gauge = Plugin.JobGauges.Get(); + + if (Config.RattlingCoilGauge.HideWhenInactive && gauge.RattlingCoilStacks <= 0) + { + return; + } + + int maxStacks = player.Level >= 88 ? 3 : 2; + BarHud[] bars = BarUtilities.GetChunkedBars(Config.RattlingCoilGauge, maxStacks, gauge.RattlingCoilStacks, maxStacks, 0, player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.RattlingCoilGauge.StrataLevel)); + } + } + + private unsafe void DrawAnguineTributeGauge(Vector2 origin, IPlayerCharacter player) + { + VPRGauge gauge = Plugin.JobGauges.Get(); + + if (Config.AnguineTribute.HideWhenInactive && gauge.AnguineTribute <= 0) + { + return; + } + + int maxStacks = player.Level >= 96 ? 5 : 4; + BarHud[] bars = BarUtilities.GetChunkedBars(Config.AnguineTribute, maxStacks, gauge.AnguineTribute, maxStacks, 0, player); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.AnguineTribute.StrataLevel)); + } + } + + private unsafe void DrawSerpentOfferingsBar(Vector2 origin, IPlayerCharacter player) + { + ViperConfig.SerpentOfferingsBarConfig config = Config.SerpentOfferings; + VPRGauge gauge = Plugin.JobGauges.Get(); + + float reawakenedDuration = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 3670 or 4094 && o.RemainingTime > 0f)?.RemainingTime ?? 0f; + bool reAwakenedReady = Utils.StatusListForBattleChara(player).Any(o => o.StatusId is 3671) || gauge.SerpentOffering >= 50; + bool isReawakened = reawakenedDuration > 0; + bool showReawakened = isReawakened && config.EnableAwakenedTimer; + + float serpentOffering = showReawakened && isReawakened ? reawakenedDuration : gauge.SerpentOffering; + + if (Config.SerpentOfferings.HideWhenInactive && gauge.SerpentOffering <= 0) + { + return; + } + + Config.SerpentOfferings.Label.SetValue(serpentOffering); + + BarHud[] bars = BarUtilities.GetChunkedProgressBars( + config, + showReawakened ? 1 : 2, + showReawakened ? reawakenedDuration : serpentOffering, + showReawakened ? 30f : 100f, + fillColor: reAwakenedReady ? config.AwakenedColor : config.FillColor + ); ; + + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.SerpentOfferings.StrataLevel)); + } + } + } + + public enum ViperCombo + { + SteelFangs = 34606, + DreadFangs = 34607, + HuntersSting = 34608, + SwiftskinsSting = 34609, + SteelMaw = 34614, + DreadMaw = 34615, + HuntersBite = 34616, + SwiftskinsBite = 34617 + } + + public enum ViperComboState + { + None, + Started, + Finisher + } + + [Section("Job Specific Bars")] + [SubSection("Melee", 0)] + [SubSection("Viper", 1)] + public class ViperConfig : JobConfig + { + [JsonIgnore] public override uint JobId => JobIDs.VPR; + + public new static ViperConfig DefaultConfig() + { + var config = new ViperConfig(); + config.SerpentOfferings.UseChunks = false; + + return config; + } + + [NestedConfig("Vipersight Bar", 30)] + public VipersightBarConfig Vipersight = new VipersightBarConfig( + new(0, -10), + new(254, 10), + new(new Vector4(237f / 255f, 141f / 255f, 7f / 255f, 100f / 100f)) + ); + + [NestedConfig("Rattling Coil Bar", 40)] + public ChunkedBarConfig RattlingCoilGauge = new ChunkedBarConfig( + new(0, -34), + new(254, 10), + new(new Vector4(204f / 255f, 40f / 255f, 40f / 255f, 1f)) + ); + + [NestedConfig("Serpent Offerings Bar", 45)] + public SerpentOfferingsBarConfig SerpentOfferings = new SerpentOfferingsBarConfig( + new(0, -46), + new(254, 10), + new(new Vector4(69f / 255f, 115f / 255f, 202f / 255f, 1f)) + ); + + [NestedConfig("Anguine Tribute Bar", 50)] + public ChunkedBarConfig AnguineTribute = new ChunkedBarConfig( + new(0, -58), + new(254, 10), + new(new Vector4(69f / 255f, 115f / 255f, 202f / 255f, 1f)) + ); + + [Exportable(false)] + public class VipersightBarConfig : ChunkedBarConfig + { + [NestedConfig("Show Glow", 39, separator = false, spacing = true)] + public BarGlowConfig GlowConfig = new BarGlowConfig(); + + [ColorEdit4("Combo Start", spacing = true)] + [Order(41)] + public PluginConfigColor ComboStartColor = new(new Vector4(230f / 255f, 33f / 255f, 33f / 255f, 100f / 100f)); + + [ColorEdit4("Flank Ender")] + [Order(42)] + public PluginConfigColor ComboEndFlankColor = new(new Vector4(46f / 255f, 228f / 255f, 42f / 255f, 1f)); + + [ColorEdit4("Hind Ender")] + [Order(43)] + public PluginConfigColor ComboEndHindColor = new(new Vector4(230f / 255f, 33f / 255f, 33f / 255f, 1f)); + + [ColorEdit4("Grim/Default Ender")] + [Order(44)] + public PluginConfigColor ComboEndAOEColor = new(new Vector4(69f / 255f, 115f / 255f, 202f / 255f, 1f)); + + [Checkbox("Invert", spacing = true)] + [Order(45)] + public bool Invert = false; + + public VipersightBarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) + : base(position, size, fillColor) + { + } + } + + [Exportable(false)] + public class SerpentOfferingsBarConfig : ChunkedProgressBarConfig + { + [Checkbox("Enable Awakened Timer", spacing = true)] + [Order(46)] + public bool EnableAwakenedTimer = true; + + [ColorEdit4("Ready to Reawaken Color")] + [Order(47)] + public PluginConfigColor AwakenedColor = new(new Vector4(69f / 255f, 115f / 255f, 202f / 255f, 1f)); + + public SerpentOfferingsBarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) + : base(position, size, fillColor) + { + } + } + } +} \ No newline at end of file diff --git a/Interface/Jobs/WarriorHud.cs b/Interface/Jobs/WarriorHud.cs new file mode 100644 index 0000000..04a0af4 --- /dev/null +++ b/Interface/Jobs/WarriorHud.cs @@ -0,0 +1,279 @@ +using Dalamud.Game.ClientState.JobGauge.Types; +using Dalamud.Game.ClientState.Objects.SubKinds; +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using HSUI.Interface.GeneralElements; + +namespace HSUI.Interface.Jobs +{ + public class WarriorHud : JobHud + { + private new WarriorConfig Config => (WarriorConfig)_config; + + public WarriorHud(WarriorConfig config, string? displayName = null) : base(config, displayName) + { + } + + protected override (List, List) ChildrenPositionsAndSizes() + { + List positions = new(); + List sizes = new(); + + if (Config.SurgingTempestBar.Enabled) + { + positions.Add(Config.Position + Config.SurgingTempestBar.Position); + sizes.Add(Config.SurgingTempestBar.Size); + } + + if (Config.BeastGauge.Enabled) + { + positions.Add(Config.Position + Config.BeastGauge.Position); + sizes.Add(Config.BeastGauge.Size); + } + + if (Config.InnerReleaseBar.Enabled) + { + positions.Add(Config.Position + Config.InnerReleaseBar.Position); + sizes.Add(Config.InnerReleaseBar.Size); + } + + return (positions, sizes); + } + + public override void DrawJobHud(Vector2 origin, IPlayerCharacter player) + { + Vector2 pos = origin + Config.Position; + + if (Config.SurgingTempestBar.Enabled) + { + DrawSurgingTempestBar(pos, player); + } + + if (Config.BeastGauge.Enabled) + { + DrawBeastGauge(pos, player); + } + + if (Config.InnerReleaseBar.Enabled) + { + DrawInnerReleaseBar(pos, player); + } + } + + private void DrawSurgingTempestBar(Vector2 origin, IPlayerCharacter player) + { + float surgingTempestDuration = Math.Abs(Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 2677)?.RemainingTime ?? 0f); + + if (!Config.SurgingTempestBar.HideWhenInactive || surgingTempestDuration > 0) + { + Config.SurgingTempestBar.Label.SetValue(surgingTempestDuration); + + BarHud bar = BarUtilities.GetProgressBar(Config.SurgingTempestBar, surgingTempestDuration, 60f, 0, player); + AddDrawActions(bar.GetDrawActions(origin, Config.SurgingTempestBar.StrataLevel)); + } + } + + private void DrawBeastGauge(Vector2 origin, IPlayerCharacter player) + { + WARGauge gauge = Plugin.JobGauges.Get(); + var nascentChaosDuration = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 1897)?.RemainingTime ?? 0f; + + if (!Config.BeastGauge.HideWhenInactive || gauge.BeastGauge > 0 || nascentChaosDuration > 0) + { + Config.BeastGauge.Label.SetValue(gauge.BeastGauge); + + var color = nascentChaosDuration == 0 ? Config.BeastGauge.BeastGaugeColor : Config.BeastGauge.NascentChaosColor; + BarHud[] bars = BarUtilities.GetChunkedProgressBars(Config.BeastGauge, 2, gauge.BeastGauge, 100, 0, player, fillColor: color); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.BeastGauge.StrataLevel)); + } + } + } + + private void DrawInnerReleaseBar(Vector2 origin, IPlayerCharacter player) + { + var innerReleaseStatus = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 1177 or 86); + + float innerReleaseDuration = Math.Max(innerReleaseStatus?.RemainingTime ?? 0f, 0f); + int innerReleaseStacks = innerReleaseStatus?.Param ?? 0; + + BarGlowConfig? primalRendGlow = null; + if (Config.InnerReleaseBar.PrimalRendReadyGlowConfig.Enabled) + { + bool isPrimalRendReady = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 2624)?.RemainingTime > 0; + if (isPrimalRendReady) + { + primalRendGlow = Config.InnerReleaseBar.PrimalRendReadyGlowConfig; + } + } + + if (innerReleaseStacks == 0 && Config.InnerReleaseBar.ShowCooldown) + { + uint spellID = 7389; + float maxDuration = SpellHelper.Instance.GetRecastTime(spellID); + float cooldown = SpellHelper.Instance.GetSpellCooldown(spellID); + float currentCooldown = maxDuration - cooldown; + + Config.InnerReleaseBar.Label.SetValue(maxDuration - currentCooldown); + + if (currentCooldown == maxDuration) + { + if (!Config.InnerReleaseBar.HideWhenInactive) + { + BarHud[] bars = BarUtilities.GetChunkedProgressBars( + Config.InnerReleaseBar, + 1, + 1, + 1, + 0, + player, + fillColor: Config.InnerReleaseBar.CooldownFinishedColor, + glowConfig: primalRendGlow, + chunksToGlow: new[] { true } + ); + + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.InnerReleaseBar.StrataLevel)); + } + } + } + else + { + BarHud[] bars = BarUtilities.GetChunkedProgressBars( + Config.InnerReleaseBar, + 1, + currentCooldown, + maxDuration, + 0, + player, + fillColor: Config.InnerReleaseBar.CooldownInProgressColor, + glowConfig: primalRendGlow, + chunksToGlow: new[] { true } + ); + + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.InnerReleaseBar.StrataLevel)); + } + } + + return; + } + + if (!Config.InnerReleaseBar.HideWhenInactive || innerReleaseStacks > 0) + { + float innerReleaseMaxDuration = 15f; + var chunks = new Tuple[3]; + + for (int i = 0; i < 3; i++) + { + chunks[i] = new(Config.InnerReleaseBar.FillColor, i < innerReleaseStacks ? 1 : 0, i == 1 ? Config.InnerReleaseBar.Label : null); + } + + innerReleaseDuration = !Config.InnerReleaseBar.ShowBuffTimerOnActiveChunk + ? innerReleaseMaxDuration + : Math.Min(innerReleaseDuration, innerReleaseMaxDuration); + + Config.InnerReleaseBar.Label.SetValue(innerReleaseDuration); + + BarHud[] bars = BarUtilities.GetChunkedBars(Config.InnerReleaseBar, chunks, player, primalRendGlow, new[] { true, true, true }); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.InnerReleaseBar.StrataLevel)); + } + } + } + } + + [Section("Job Specific Bars")] + [SubSection("Tank", 0)] + [SubSection("Warrior", 1)] + public class WarriorConfig : JobConfig + { + [JsonIgnore] public override uint JobId => JobIDs.WAR; + public new static WarriorConfig DefaultConfig() + { + var config = new WarriorConfig(); + + config.BeastGauge.UsePartialFillColor = true; + config.InnerReleaseBar.LabelMode = LabelMode.ActiveChunk; + config.InnerReleaseBar.PrimalRendReadyGlowConfig.Color = new PluginConfigColor(new Vector4(246f / 255f, 30f / 255f, 136f / 255f, 100f / 100f)); + + return config; + } + + [NestedConfig("Surging Tempest Bar", 30)] + public ProgressBarConfig SurgingTempestBar = new ProgressBarConfig( + new(0, -32), + new(254, 20), + new PluginConfigColor(new Vector4(255f / 255f, 136f / 255f, 146f / 255f, 100f / 100f)) + ); + + [NestedConfig("Beast Gauge", 35)] + public WarriorBeastGaugeConfig BeastGauge = new WarriorBeastGaugeConfig( + new(0, -10), + new(254, 20), + new PluginConfigColor(new Vector4(0, 0, 0, 0)) + ); + + [NestedConfig("Inner Release Bar", 40)] + public WarriorInnerReleaseBarConfig InnerReleaseBar = new WarriorInnerReleaseBarConfig( + new(0, -54), + new(254, 20), + new PluginConfigColor(new Vector4(255f / 255f, 136f / 255f, 146f / 255f, 100f / 100f)) + ); + } + + [DisableParentSettings("FillColor")] + [Exportable(false)] + public class WarriorBeastGaugeConfig : ChunkedProgressBarConfig + { + [ColorEdit4("Beast Gauge Color", spacing = true)] + [Order(65)] + public PluginConfigColor BeastGaugeColor = new(new Vector4(201f / 255f, 13f / 255f, 13f / 255f, 100f / 100f)); + + [ColorEdit4("Nascent Chaos Color")] + [Order(70)] + public PluginConfigColor NascentChaosColor = new(new Vector4(240f / 255f, 176f / 255f, 0f / 255f, 100f / 100f)); + + public WarriorBeastGaugeConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) : base(position, size, fillColor) + { + } + } + + [Exportable(false)] + public class WarriorInnerReleaseBarConfig : ChunkedProgressBarConfig + { + [Checkbox("Show Buff Timer On Active Chunk", spacing = true)] + [Order(80)] + public bool ShowBuffTimerOnActiveChunk; + + [Checkbox("Show Inner Release Cooldown", spacing = true)] + [Order(85)] + public bool ShowCooldown; + + [ColorEdit4("Inner Release On Cooldown Color")] + [Order(90)] + public PluginConfigColor CooldownInProgressColor = new(new Vector4(240f / 255f, 176f / 255f, 0f / 255f, 100f / 100f)); + + [ColorEdit4("Inner Release Ready Color")] + [Order(95)] + public PluginConfigColor CooldownFinishedColor = new(new Vector4(38f / 255f, 192f / 255f, 94f / 255f, 100f / 100f)); + + [NestedConfig("Glow Color (when Primal Rend is ready)", 100, separator = false, spacing = true)] + public BarGlowConfig PrimalRendReadyGlowConfig = new(); + + public WarriorInnerReleaseBarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) : base(position, size, fillColor) + { + } + } +} diff --git a/Interface/Jobs/WhiteMageHud.cs b/Interface/Jobs/WhiteMageHud.cs new file mode 100644 index 0000000..56f44b5 --- /dev/null +++ b/Interface/Jobs/WhiteMageHud.cs @@ -0,0 +1,298 @@ +using Dalamud.Game.ClientState.JobGauge.Types; +using Dalamud.Game.ClientState.Objects.SubKinds; +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using HSUI.Interface.GeneralElements; +using Newtonsoft.Json; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace HSUI.Interface.Jobs +{ + public class WhiteMageHud : JobHud + { + private new WhiteMageConfig Config => (WhiteMageConfig)_config; + + private static readonly List DiaIDs = new() { 143, 144, 1871 }; + private static readonly List DiaDurations = new() { 30, 30, 30 }; + + public WhiteMageHud(WhiteMageConfig config, string? displayName = null) : base(config, displayName) + { + } + + protected override (List, List) ChildrenPositionsAndSizes() + { + List positions = new(); + List sizes = new(); + + if (Config.LilyBar.Enabled) + { + positions.Add(Config.Position + Config.LilyBar.Position); + sizes.Add(Config.LilyBar.Size); + } + + if (Config.DiaBar.Enabled) + { + positions.Add(Config.Position + Config.DiaBar.Position); + sizes.Add(Config.DiaBar.Size); + } + + if (Config.AsylumBar.Enabled) + { + positions.Add(Config.Position + Config.AsylumBar.Position); + sizes.Add(Config.AsylumBar.Size); + } + + if (Config.PresenceOfMindBar.Enabled) + { + positions.Add(Config.Position + Config.PresenceOfMindBar.Position); + sizes.Add(Config.PresenceOfMindBar.Size); + } + + if (Config.PlenaryBar.Enabled) + { + positions.Add(Config.Position + Config.PlenaryBar.Position); + sizes.Add(Config.PlenaryBar.Size); + } + + if (Config.TemperanceBar.Enabled) + { + positions.Add(Config.Position + Config.TemperanceBar.Position); + sizes.Add(Config.TemperanceBar.Size); + } + + return (positions, sizes); + } + + public override void DrawJobHud(Vector2 origin, IPlayerCharacter player) + { + Vector2 pos = origin + Config.Position; + + if (Config.LilyBar.Enabled) + { + DrawLilyBar(pos, player); + } + + if (Config.DiaBar.Enabled) + { + DrawDiaBar(pos, player); + } + + if (Config.AsylumBar.Enabled) + { + DrawAsylumBar(pos, player); + } + + if (Config.PresenceOfMindBar.Enabled) + { + DrawPresenceOfMindBar(pos, player); + } + + if (Config.PlenaryBar.Enabled) + { + DrawPlenaryBar(pos, player); + + } + if (Config.TemperanceBar.Enabled) + { + DrawTemperanceBar(pos, player); + } + } + + private void DrawDiaBar(Vector2 origin, IPlayerCharacter player) + { + var target = Plugin.TargetManager.SoftTarget ?? Plugin.TargetManager.Target; + + BarHud? bar = BarUtilities.GetDoTBar(Config.DiaBar, player, target, DiaIDs, DiaDurations); + if (bar != null) + { + AddDrawActions(bar.GetDrawActions(origin, Config.DiaBar.StrataLevel)); + } + } + + private void DrawLilyBar(Vector2 origin, IPlayerCharacter player) + { + WHMGauge gauge = Plugin.JobGauges.Get(); + + const float lilyCooldown = 20000f; + + float GetScale(int num, float timer) => num + (timer / lilyCooldown); + float lilyScale = GetScale(gauge.Lily, gauge.LilyTimer); + + if (!Config.LilyBar.HideWhenInactive || lilyScale > 0) + { + BarHud[] bars = BarUtilities.GetChunkedBars(Config.LilyBar, 3, lilyScale, 3, 0, player, partialFillColor: Config.LilyBar.PartialFillColor); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.LilyBar.StrataLevel)); + } + } + + if (!Config.BloodLilyBar.HideWhenInactive && Config.BloodLilyBar.Enabled || gauge.BloodLily > 0) + { + BarGlowConfig? glow = gauge.BloodLily == 3 ? Config.BloodLilyBar.GlowConfig : null; + BarHud[] bars = BarUtilities.GetChunkedBars(Config.BloodLilyBar, 3, gauge.BloodLily, 3, 0, player, glowConfig: glow, chunksToGlow: new[] { true, true, true }); + foreach (BarHud bar in bars) + { + AddDrawActions(bar.GetDrawActions(origin, Config.BloodLilyBar.StrataLevel)); + } + } + } + + private void DrawAsylumBar(Vector2 origin, IPlayerCharacter player) + { + float asylymDuration = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 739 or 1911 && o.SourceId == player.GameObjectId)?.RemainingTime ?? 0f; + + if (!Config.AsylumBar.HideWhenInactive || asylymDuration > 0) + { + Config.AsylumBar.Label.SetValue(asylymDuration); + + BarHud bar = BarUtilities.GetProgressBar(Config.AsylumBar, asylymDuration, 24f, 0f, player); + AddDrawActions(bar.GetDrawActions(origin, Config.AsylumBar.StrataLevel)); + } + } + + private void DrawPresenceOfMindBar(Vector2 origin, IPlayerCharacter player) + { + float presenceOfMindDuration = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 157 && o.SourceId == player.GameObjectId)?.RemainingTime ?? 0f; + + if (!Config.PresenceOfMindBar.HideWhenInactive || presenceOfMindDuration > 0) + { + Config.PresenceOfMindBar.Label.SetValue(presenceOfMindDuration); + + BarHud bar = BarUtilities.GetProgressBar(Config.PresenceOfMindBar, presenceOfMindDuration, 15f, 0f, player); + AddDrawActions(bar.GetDrawActions(origin, Config.PresenceOfMindBar.StrataLevel)); + } + } + + private void DrawPlenaryBar(Vector2 origin, IPlayerCharacter player) + { + float plenaryDuration = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 1219 && o.SourceId == player.GameObjectId)?.RemainingTime ?? 0f; + + if (!Config.PlenaryBar.HideWhenInactive || plenaryDuration > 0) + { + Config.PlenaryBar.Label.SetValue(plenaryDuration); + + BarHud bar = BarUtilities.GetProgressBar(Config.PlenaryBar, plenaryDuration, 10f, 0f, player); + AddDrawActions(bar.GetDrawActions(origin, Config.PlenaryBar.StrataLevel)); + } + } + + private void DrawTemperanceBar(Vector2 origin, IPlayerCharacter player) + { + float temperanceDuration = Utils.StatusListForBattleChara(player).FirstOrDefault(o => o.StatusId is 1872 && o.SourceId == player.GameObjectId)?.RemainingTime ?? 0f; + + if (!Config.TemperanceBar.HideWhenInactive || temperanceDuration > 0) + { + Config.TemperanceBar.Label.SetValue(temperanceDuration); + + BarHud bar = BarUtilities.GetProgressBar(Config.TemperanceBar, temperanceDuration, 20f, 0f, player); + AddDrawActions(bar.GetDrawActions(origin, Config.TemperanceBar.StrataLevel)); + } + } + } + + [Section("Job Specific Bars")] + [SubSection("Healer", 0)] + [SubSection("White Mage", 1)] + public class WhiteMageConfig : JobConfig + { + [JsonIgnore] public override uint JobId => JobIDs.WHM; + public new static WhiteMageConfig DefaultConfig() + { + var config = new WhiteMageConfig(); + + config.UseDefaultPrimaryResourceBar = true; + + config.AsylumBar.Label.FontID = FontsConfig.DefaultMediumFontKey; + config.PresenceOfMindBar.Label.FontID = FontsConfig.DefaultMediumFontKey; + config.PlenaryBar.Label.FontID = FontsConfig.DefaultMediumFontKey; + config.TemperanceBar.Label.FontID = FontsConfig.DefaultMediumFontKey; + + return config; + } + + [NestedConfig("Lily Bar", 30)] + public LilyBarConfig LilyBar = new LilyBarConfig( + new(-64, -32), + new(126, 20), + new PluginConfigColor(new(0f / 255f, 64f / 255f, 255f / 255f, 100f / 100f)) + ); + + [NestedConfig("Blood Lily Bar", 35)] + public BloodLilyBarConfig BloodLilyBar = new BloodLilyBarConfig( + new(64, -32), + new(126, 20), + new PluginConfigColor(new(199f / 255f, 40f / 255f, 9f / 255f, 100f / 100f)) + ); + + [NestedConfig("Dia Bar", 40)] + public ProgressBarConfig DiaBar = new ProgressBarConfig( + new(0, -10), + new(254, 20), + new PluginConfigColor(new(0f / 255f, 64f / 255f, 255f / 255f, 100f / 100f)) + ); + + [NestedConfig("Asylum Bar", 45)] + public ProgressBarConfig AsylumBar = new ProgressBarConfig( + new(-96, -52), + new(62, 15), + new PluginConfigColor(new(241f / 255f, 217f / 255f, 125f / 255f, 100f / 100f)) + ); + + [NestedConfig("Presence of Mind Bar", 50)] + public ProgressBarConfig PresenceOfMindBar = new ProgressBarConfig( + new(-32, -52), + new(62, 15), + new PluginConfigColor(new(213f / 255f, 124f / 255f, 97f / 255f, 100f / 100f)) + ); + + [NestedConfig("Plenary Bar", 55)] + public ProgressBarConfig PlenaryBar = new ProgressBarConfig( + new(32, -52), + new(62, 15), + new PluginConfigColor(new(26f / 255f, 167f / 255f, 109f / 255f, 100f / 100f)) + ); + + [NestedConfig("Temperance Bar", 60)] + public ProgressBarConfig TemperanceBar = new ProgressBarConfig( + new(96, -52), + new(62, 15), + new PluginConfigColor(new(100f / 255f, 207f / 255f, 211f / 255f, 100f / 100f)) + ); + } + + [Exportable(false)] + public class LilyBarConfig : ChunkedBarConfig + { + [Checkbox("Use Partial Fill Color", spacing = true)] + [Order(65)] + public bool UsePartialFillColor = false; + + [ColorEdit4("Partial Fill Color")] + [Order(66, collapseWith = nameof(UsePartialFillColor))] + public PluginConfigColor PartialFillColor; + + public LilyBarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) + : base(position, size, fillColor) + { + PartialFillColor = new PluginConfigColor(new(0f / 255f, 64f / 255f, 255f / 255f, 50f / 100f)); + } + } + + [Exportable(false)] + public class BloodLilyBarConfig : ChunkedBarConfig + { + [NestedConfig("Glow Color (when Misery ready)", 60, separator = false, spacing = true)] + public BarGlowConfig GlowConfig = new(); + + public BloodLilyBarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor) + : base(position, size, fillColor) + { + GlowConfig.Color = new PluginConfigColor(new(247f / 255f, 177f / 255f, 67f / 255f, 100f / 100f)); + } + } +} diff --git a/Interface/Nameplates/Nameplate.cs b/Interface/Nameplates/Nameplate.cs new file mode 100644 index 0000000..e6935ef --- /dev/null +++ b/Interface/Nameplates/Nameplate.cs @@ -0,0 +1,639 @@ +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.Helpers; +using HSUI.Interface.Bars; +using HSUI.Interface.GeneralElements; +using HSUI.Interface.StatusEffects; +using Dalamud.Bindings.ImGui; +using System.Collections.Generic; +using System.Numerics; +using Action = System.Action; +using Character = Dalamud.Game.ClientState.Objects.Types.ICharacter; +using StructsCharacter = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; + +namespace HSUI.Interface.Nameplates +{ + public class Nameplate + { + protected NameplateConfig _config; + public bool Enabled => _config.Enabled; + + protected LabelHud _nameLabelHud; + protected LabelHud _titleLabelHud; + + public Nameplate(NameplateConfig config) + { + _config = config; + + _nameLabelHud = new LabelHud(config.NameLabelConfig); + _titleLabelHud = new LabelHud(config.TitleLabelConfig); + } + + protected bool IsVisible(IGameObject? actor) + { + if (!_config.Enabled || + actor == null || + !_config.VisibilityConfig.IsElementVisible(null) || + (_config.OnlyShowWhenTargeted && actor.Address != Plugin.TargetManager.Target?.Address)) + { + return false; + } + + return true; + } + + public virtual List<(StrataLevel, Action)> GetElementsDrawActions(NameplateData data) + { + List<(StrataLevel, Action)> drawActions = new List<(StrataLevel, Action)>(); + if (!IsVisible(data.GameObject)) { return drawActions; } + + drawActions.AddRange(GetMainLabelDrawActions(data)); + + return drawActions; + } + + protected List<(StrataLevel, Action)> GetMainLabelDrawActions(NameplateData data, NameplateAnchor? barAnchor = null) + { + List<(StrataLevel, Action)> drawActions = new List<(StrataLevel, Action)>(); + Vector2 origin = _config.Position + (barAnchor?.Position ?? data.ScreenPosition); + + Vector2 swapOffset = Vector2.Zero; + if (_config.SwapLabelsWhenNeeded && (data.IsTitlePrefix || data.Title.Length == 0)) + { + swapOffset = _config.TitleLabelConfig.Position - _config.NameLabelConfig.Position; + } + + // name + float nameAlpha = _config.RangeConfig.AlphaForDistance(data.Distance, _config.NameLabelConfig.Color.Vector.W); + var (nameText, namePos, nameSize, nameColor) = _nameLabelHud.PreCalculate(origin + swapOffset, barAnchor?.Size, data.GameObject, data.Name, isPlayerName: data.Kind == ObjectKind.Player); + drawActions.Add((_config.NameLabelConfig.StrataLevel, () => + { + _nameLabelHud.DrawLabel(nameText, namePos, nameSize, nameColor, nameAlpha); + } + )); + + // title + float titleAlpha = _config.RangeConfig.AlphaForDistance(data.Distance, _config.TitleLabelConfig.Color.Vector.W); + var (titleText, titlePos, titleSize, titleColor) = _titleLabelHud.PreCalculate(origin - swapOffset, barAnchor?.Size, data.GameObject, title: data.Title); + if (data.Title.Length > 0) + { + drawActions.Add((_config.TitleLabelConfig.StrataLevel, () => + { + _titleLabelHud.DrawLabel(titleText, titlePos, titleSize, titleColor, titleAlpha); + } + )); + } + + return drawActions; + } + } + + public class NameplateWithBar : Nameplate + { + protected NameplateBarConfig BarConfig => ((NameplateWithBarConfig)_config).GetBarConfig(); + + private LabelHud _leftLabelHud; + private LabelHud _rightLabelHud; + private LabelHud _optionalLabelHud; + + public NameplateWithBar(NameplateConfig config) : base(config) + { + _leftLabelHud = new LabelHud(BarConfig.LeftLabelConfig); + _rightLabelHud = new LabelHud(BarConfig.RightLabelConfig); + _optionalLabelHud = new LabelHud(BarConfig.OptionalLabelConfig); + } + + public (bool, bool) GetMouseoverState(NameplateData data) + { + if (data.GameObject is not ICharacter character) { return (false, false); } + if (!BarConfig.IsVisible(character.CurrentHp, character.MaxHp) || BarConfig.DisableInteraction) + { + return (false, false); + } + + bool targeted = Plugin.TargetManager.Target?.Address == character.Address; + Vector2 barSize = BarConfig.GetSize(targeted); + + Vector2 origin = _config.Position + data.ScreenPosition; + Vector2 barPos = Utils.GetAnchoredPosition(origin, barSize, BarConfig.Anchor) + BarConfig.Position; + var (areaStart, areaEnd) = BarConfig.MouseoverAreaConfig.GetArea(barPos, barSize); + + bool isHovering = ImGui.IsMouseHoveringRect(areaStart, areaEnd); + bool ignoreMouseover = BarConfig.MouseoverAreaConfig.Enabled && BarConfig.MouseoverAreaConfig.Ignore; + + return (isHovering, ignoreMouseover); + } + + public unsafe List<(StrataLevel, Action)> GetBarDrawActions(NameplateData data) + { + List<(StrataLevel, Action)> drawActions = new List<(StrataLevel, Action)>(); + if (!IsVisible(data.GameObject)) { return drawActions; } + if (data.GameObject is not Character character) { return drawActions; } + + uint currentHp = character.CurrentHp; + uint maxHp = character.MaxHp; + + if (!BarConfig.IsVisible(currentHp, maxHp)) { return drawActions; } + + // colors + PluginConfigColor fillColor = GetFillColor(character, currentHp, maxHp); + fillColor = fillColor.WithAlpha(_config.RangeConfig.AlphaForDistance(data.Distance, fillColor.Vector.W)); + + PluginConfigColor bgColor = GetBackgroundColor(character); + bgColor = bgColor.WithAlpha(_config.RangeConfig.AlphaForDistance(data.Distance, bgColor.Vector.W)); + + bool targeted = character.Address == Plugin.TargetManager.Target?.Address; + PluginConfigColor borderColor = targeted ? BarConfig.TargetedBorderColor : BarConfig.BorderColor; + borderColor = borderColor.WithAlpha( + _config.RangeConfig.AlphaForDistance(data.Distance, BarConfig.BorderColor.Vector.W) + ); + + // bar + Vector2 barSize = BarConfig.GetSize(targeted); + Rect background = new Rect(BarConfig.Position, barSize, bgColor); + Rect healthFill = BarUtilities.GetFillRect(BarConfig.Position, barSize, BarConfig.FillDirection, fillColor, currentHp, maxHp); + + BarHud bar = new BarHud( + BarConfig.ID, + BarConfig.DrawBorder, + borderColor, + targeted ? BarConfig.TargetedBorderThickness : BarConfig.BorderThickness, + BarConfig.Anchor, + character, + current: currentHp, + max: maxHp, + shadowConfig: BarConfig.ShadowConfig, + barTextureName: BarConfig.BarTextureName, + barTextureDrawMode: BarConfig.BarTextureDrawMode + ); + + bar.NeedsInputs = true; + bar.SetBackground(background); + bar.AddForegrounds(healthFill); + + // shield + PluginConfigColor shieldColor = BarConfig.ShieldConfig.Color.WithAlpha( + _config.RangeConfig.AlphaForDistance(data.Distance, BarConfig.ShieldConfig.Color.Vector.W) + ); + BarUtilities.AddShield(bar, BarConfig, BarConfig.ShieldConfig, character, healthFill.Size, shieldColor); + + // draw bar + Vector2 origin = _config.Position + data.ScreenPosition; + drawActions.AddRange(bar.GetDrawActions(origin, _config.StrataLevel)); + + // mouseover area + BarHud? mouseoverAreaBar = BarConfig.MouseoverAreaConfig.GetBar( + BarConfig.Position, + barSize, + BarConfig.ID + "_mouseoverArea", + BarConfig.Anchor + ); + + if (mouseoverAreaBar != null) + { + drawActions.AddRange(mouseoverAreaBar.GetDrawActions(origin, StrataLevel.HIGHEST)); + } + + // labels + Vector2 barPos = Utils.GetAnchoredPosition(origin, barSize, BarConfig.Anchor) + BarConfig.Position; + LabelHud[] labels = GetLabels(maxHp); + foreach (LabelHud label in labels) + { + LabelConfig labelConfig = (LabelConfig)label.GetConfig(); + float alpha = _config.RangeConfig.AlphaForDistance(data.Distance, labelConfig.Color.Vector.W); + var (labelText, labelPos, labelSize, labelColor) = label.PreCalculate(barPos, barSize, data.GameObject, data.Name, currentHp, maxHp, data.Kind == ObjectKind.Player); + + drawActions.Add((labelConfig.StrataLevel, () => + { + label.DrawLabel(labelText, labelPos, labelSize, labelColor, alpha); + } + )); + } + + return drawActions; + } + + public override List<(StrataLevel, Action)> GetElementsDrawActions(NameplateData data) + { + List<(StrataLevel, Action)> drawActions = new List<(StrataLevel, Action)>(); + if (!IsVisible(data.GameObject)) { return drawActions; } + + NameplateAnchor? barAnchor = GetBarAnchor(data); + drawActions.AddRange(GetMainLabelDrawActions(data, barAnchor)); + + return drawActions; + } + + protected virtual NameplateAnchor? GetBarAnchor(NameplateData data) + { + if (data.GameObject is Character chara && + BarConfig.IsVisible(chara.CurrentHp, chara.MaxHp)) + { + bool targeted = Plugin.TargetManager.Target?.Address == data.GameObject.Address; + Vector2 size = BarConfig.GetSize(targeted); + Vector2 pos = Utils.GetAnchoredPosition(data.ScreenPosition + BarConfig.Position, size, BarConfig.Anchor); + + return new NameplateAnchor(pos, size); + } + + return null; + } + + private LabelHud[] GetLabels(uint maxHp) + { + List labels = new List(); + + if (BarConfig.HideHealthIfPossible && maxHp <= 0) + { + if (!Utils.IsHealthLabel(BarConfig.LeftLabelConfig)) + { + labels.Add(_leftLabelHud); + } + + if (!Utils.IsHealthLabel(BarConfig.RightLabelConfig)) + { + labels.Add(_rightLabelHud); + } + + if (!Utils.IsHealthLabel(BarConfig.OptionalLabelConfig)) + { + labels.Add(_optionalLabelHud); + } + } + else + { + labels.Add(_leftLabelHud); + labels.Add(_rightLabelHud); + labels.Add(_optionalLabelHud); + } + + return labels.ToArray(); + } + + protected virtual PluginConfigColor GetFillColor(Character character, uint currentHp, uint maxHp) + { + return ColorUtils.ColorForCharacter( + character, + currentHp, + maxHp, + false, + false, + BarConfig.ColorByHealth + ) ?? BarConfig.FillColor; + } + + protected virtual PluginConfigColor GetBackgroundColor(Character character) + { + return BarConfig.BackgroundColor; + } + } + + public class NameplateWithBarAndExtras : NameplateWithBar + { + public NameplateWithBarAndExtras(NameplateConfig config) : base(config) + { + } + + public override List<(StrataLevel, Action)> GetElementsDrawActions(NameplateData data) + { + List<(StrataLevel, Action)> drawActions = new List<(StrataLevel, Action)>(); + if (!IsVisible(data.GameObject)) { return drawActions; } + + NameplateAnchor? barAnchor = GetBarAnchor(data); + Vector2 origin = _config.Position + (barAnchor?.Position ?? data.ScreenPosition); + + Vector2 swapOffset = Vector2.Zero; + if (_config.SwapLabelsWhenNeeded && (data.IsTitlePrefix || data.Title.Length == 0)) + { + swapOffset = _config.TitleLabelConfig.Position - _config.NameLabelConfig.Position; + } + + // name + float nameAlpha = _config.RangeConfig.AlphaForDistance(data.Distance, _config.NameLabelConfig.Color.Vector.W); + var (nameText, namePos, nameSize, nameColor) = _nameLabelHud.PreCalculate(origin + swapOffset, barAnchor?.Size, data.GameObject, data.Name, isPlayerName: data.Kind == ObjectKind.Player); + drawActions.Add((_config.NameLabelConfig.StrataLevel, () => + { + _nameLabelHud.DrawLabel(nameText, namePos, nameSize, nameColor, nameAlpha); + } + )); + + // title + float titleAlpha = _config.RangeConfig.AlphaForDistance(data.Distance, _config.TitleLabelConfig.Color.Vector.W); + var (titleText, titlePos, titleSize, titleColor) = _titleLabelHud.PreCalculate(origin - swapOffset, barAnchor?.Size, data.GameObject, title: data.Title); + if (data.Title.Length > 0) + { + drawActions.Add((_config.TitleLabelConfig.StrataLevel, () => + { + _titleLabelHud.DrawLabel(titleText, titlePos, titleSize, titleColor, titleAlpha); + } + )); + } + + // extras anchor + NameplateExtrasAnchors extrasAnchors = new NameplateExtrasAnchors( + barAnchor, + _config.NameLabelConfig.Enabled ? new NameplateAnchor(namePos, nameSize) : null, + _config.TitleLabelConfig.Enabled && data.Title.Length > 0 ? new NameplateAnchor(titlePos, titleSize) : null + ); + + drawActions.AddRange(GetExtrasDrawActions(data, extrasAnchors)); + + return drawActions; + } + + protected virtual List<(StrataLevel, Action)> GetExtrasDrawActions(NameplateData data, NameplateExtrasAnchors anchors) + { + // override + return new List<(StrataLevel, Action)>(); + } + } + + public class NameplateWithPlayerBar : NameplateWithBarAndExtras + { + private NameplateWithPlayerBarConfig Config => (NameplateWithPlayerBarConfig)_config; + + public NameplateWithPlayerBar(NameplateWithPlayerBarConfig config) : base(config) + { + } + + protected override List<(StrataLevel, Action)> GetExtrasDrawActions(NameplateData data, NameplateExtrasAnchors anchors) + { + List<(StrataLevel, Action)> drawActions = new List<(StrataLevel, Action)>(); + if (data.GameObject is not IPlayerCharacter character) { return drawActions; } + + float alpha = _config.RangeConfig.AlphaForDistance(data.Distance); + + // role/job icon + if (Config.RoleIconConfig.Enabled) + { + NameplateAnchor? anchor = anchors.GetAnchor(Config.RoleIconConfig.NameplateLabelAnchor, Config.RoleIconConfig.PrioritizeHealthBarAnchor); + anchor = anchor ?? new NameplateAnchor(data.ScreenPosition, Vector2.Zero); + + uint jobId = character.ClassJob.RowId; + uint iconId = Config.RoleIconConfig.UseRoleIcons ? + JobsHelper.RoleIconIDForJob(jobId, Config.RoleIconConfig.UseSpecificDPSRoleIcons) : + JobsHelper.IconIDForJob(jobId, (uint)Config.RoleIconConfig.Style); + + if (iconId > 0) + { + var pos = Utils.GetAnchoredPosition(anchor.Value.Position, -anchor.Value.Size, Config.RoleIconConfig.FrameAnchor); + var iconPos = Utils.GetAnchoredPosition(pos + Config.RoleIconConfig.Position, Config.RoleIconConfig.Size, Config.RoleIconConfig.Anchor); + + drawActions.Add((Config.RoleIconConfig.StrataLevel, () => + { + DrawHelper.DrawInWindow(_config.ID + "_jobIcon", iconPos, Config.RoleIconConfig.Size, false, (drawList) => + { + DrawHelper.DrawIcon(iconId, iconPos, Config.RoleIconConfig.Size, false, alpha, drawList); + }); + } + )); + } + } + + // state icon + if (Config.StateIconConfig.Enabled && + data.NamePlateIconId > 0 && + Config.StateIconConfig.ShouldDrawIcon(data.NamePlateIconId)) + { + NameplateAnchor? anchor = anchors.GetAnchor(Config.StateIconConfig.NameplateLabelAnchor, Config.StateIconConfig.PrioritizeHealthBarAnchor); + anchor = anchor ?? new NameplateAnchor(data.ScreenPosition, Vector2.Zero); + + var pos = Utils.GetAnchoredPosition(anchor.Value.Position, -anchor.Value.Size, Config.StateIconConfig.FrameAnchor); + var iconPos = Utils.GetAnchoredPosition(pos + Config.StateIconConfig.Position, Config.StateIconConfig.Size, Config.StateIconConfig.Anchor); + + drawActions.Add((Config.StateIconConfig.StrataLevel, () => + { + DrawHelper.DrawInWindow(_config.ID + "_stateIcon", iconPos, Config.StateIconConfig.Size, false, (drawList) => + { + DrawHelper.DrawIcon((uint)data.NamePlateIconId, iconPos, Config.StateIconConfig.Size, false, alpha, drawList); + }); + } + )); + } + + return drawActions; + } + + protected override PluginConfigColor GetFillColor(Character character, uint currentHp, uint maxHp) + { + NameplatePlayerBarConfig config = (NameplatePlayerBarConfig)BarConfig; + + return ColorUtils.ColorForCharacter( + character, + currentHp, + maxHp, + config.UseJobColor, + config.UseRoleColor, + config.ColorByHealth + ) ?? config.FillColor; + } + + protected override PluginConfigColor GetBackgroundColor(Character character) + { + NameplatePlayerBarConfig config = (NameplatePlayerBarConfig)BarConfig; + + if (config.UseJobColorAsBackgroundColor) + { + return GlobalColors.Instance.SafeColorForJobId(character.ClassJob.RowId); + } + else if (config.UseRoleColorAsBackgroundColor) + { + return GlobalColors.Instance.SafeRoleColorForJobId(character.ClassJob.RowId); + } + + return config.BackgroundColor; + } + } + + public class NameplateWithEnemyBar : NameplateWithBarAndExtras + { + private NameplateWithEnemyBarConfig Config => (NameplateWithEnemyBarConfig)_config; + + private LabelHud _orderLabelHud; + private StatusEffectsListHud _debuffsHud; + private NameplateCastbarHud _castbarHud; + + public NameplateWithEnemyBar(NameplateWithEnemyBarConfig config) : base(config) + { + _orderLabelHud = new LabelHud(config.BarConfig.OrderLabelConfig); + _debuffsHud = new StatusEffectsListHud(config.DebuffsConfig); + _castbarHud = new NameplateCastbarHud(config.CastbarConfig); + } + + public void StopPreview() + { + _debuffsHud.StopPreview(); + _castbarHud.StopPreview(); + } + + protected override List<(StrataLevel, Action)> GetExtrasDrawActions(NameplateData data, NameplateExtrasAnchors anchors) + { + List<(StrataLevel, Action)> drawActions = new List<(StrataLevel, Action)>(); + if (data.GameObject is not Character character) { return drawActions; } + + NameplateEnemyBarConfig barConfig = Config.BarConfig; + NameplateAnchor? anchor = barConfig.IsVisible(character.CurrentHp, character.MaxHp) ? anchors.BarAnchor : anchors.NameLabelAnchor; + anchor = anchor ?? new NameplateAnchor(_config.Position + data.ScreenPosition, Vector2.Zero); + + // order label + float alpha = _config.RangeConfig.AlphaForDistance(data.Distance, barConfig.OrderLabelConfig.Color.Vector.W); + + barConfig.OrderLabelConfig.SetText(data.Order); + var (labelText, labelPos, labelSize, labelColor) = _orderLabelHud.PreCalculate(anchor.Value.Position, anchor.Value.Size, data.GameObject); + drawActions.Add((barConfig.OrderLabelConfig.StrataLevel, () => + { + _orderLabelHud.DrawLabel(labelText, labelPos, labelSize, labelColor, alpha); + } + )); + + // debuffs + Vector2 buffsPos = Utils.GetAnchoredPosition(anchor.Value.Position, -anchor.Value.Size, Config.DebuffsConfig.HealthBarAnchor); + drawActions.Add((Config.DebuffsConfig.StrataLevel, () => + { + _debuffsHud.Actor = character; + _debuffsHud.PrepareForDraw(buffsPos); + _debuffsHud.Draw(buffsPos); + } + )); + + // castbar + Vector2 castbarPos = Utils.GetAnchoredPosition(anchor.Value.Position, -anchor.Value.Size, Config.CastbarConfig.HealthBarAnchor); + drawActions.Add((Config.CastbarConfig.StrataLevel, () => + { + _castbarHud.ParentSize = anchor.Value.Size; + _castbarHud.Actor = character; + _castbarHud.PrepareForDraw(castbarPos); + _castbarHud.Draw(castbarPos); + } + )); + + // icon + if (Config.IconConfig.Enabled && data.NamePlateIconId > 0) + { + anchor = anchors.GetAnchor(Config.IconConfig.NameplateLabelAnchor, Config.IconConfig.PrioritizeHealthBarAnchor); + anchor = anchor ?? new NameplateAnchor(data.ScreenPosition, Vector2.Zero); + + var pos = Utils.GetAnchoredPosition(_config.Position + anchor.Value.Position, -anchor.Value.Size, Config.IconConfig.FrameAnchor); + var iconPos = Utils.GetAnchoredPosition(pos + Config.IconConfig.Position, Config.IconConfig.Size, Config.IconConfig.Anchor); + + drawActions.Add((Config.IconConfig.StrataLevel, () => + { + DrawHelper.DrawInWindow(_config.ID + "_enemyIcon", iconPos, Config.IconConfig.Size, false, (drawList) => + { + DrawHelper.DrawIcon((uint)data.NamePlateIconId, iconPos, Config.IconConfig.Size, false, alpha, drawList); + }); + } + )); + + } + + return drawActions; + } + + protected override unsafe PluginConfigColor GetFillColor(Character character, uint currentHp, uint maxHp) + { + NameplateEnemyBarConfig config = (NameplateEnemyBarConfig)BarConfig; + + bool targetingPlayer = character.TargetObjectId == Plugin.ObjectTable.LocalPlayer?.GameObjectId; + if (targetingPlayer && config.UseCustomColorWhenBeingTargeted) + { + return config.CustomColorWhenBeingTargeted; + } + + if (config.UseStateColor) + { + StructsCharacter* chara = (StructsCharacter*)character.Address; + byte nameplateColorId = chara->GetNamePlateColorType(); + + switch (nameplateColorId) { + case 7: return (character.StatusFlags & StatusFlags.Hostile) != 0 ? config.UnengagedHostileColor : config.UnengagedColor; + case 9: return config.EngagedColor; + case 10: return config.ClaimedColor; + case 11: return config.UnclaimedColor; + default: break; + } + } + + return base.GetFillColor(character, currentHp, maxHp); + } + } + + #region utils + public struct NameplateAnchor + { + public Vector2 Position; + public Vector2 Size; + + internal NameplateAnchor(Vector2 position, Vector2 size) + { + Position = position; + Size = size; + } + } + + public struct NameplateExtrasAnchors + { + public NameplateAnchor? BarAnchor; + public NameplateAnchor? NameLabelAnchor; + public NameplateAnchor? TitleLabelAnchor; + public NameplateAnchor? HighestLabelAnchor; + public NameplateAnchor? LowestLabelAnchor; + private NameplateAnchor? DefaultLabelAnchor; + + internal NameplateExtrasAnchors(NameplateAnchor? barAnchor, NameplateAnchor? nameLabelAnchor, NameplateAnchor? titleLabelAnchor) + { + BarAnchor = barAnchor; + NameLabelAnchor = nameLabelAnchor; + TitleLabelAnchor = titleLabelAnchor; + DefaultLabelAnchor = nameLabelAnchor; + + float nameY = -1; + if (nameLabelAnchor.HasValue) + { + nameY = nameLabelAnchor.Value.Position.Y; + } + + float titleY = -1; + if (titleLabelAnchor.HasValue) + { + titleY = titleLabelAnchor.Value.Position.Y; + } + + if (nameY == -1) + { + DefaultLabelAnchor = titleLabelAnchor; + } + else if (nameY < titleY) + { + HighestLabelAnchor = nameLabelAnchor; + LowestLabelAnchor = titleLabelAnchor; + } + else if (nameY > titleY) + { + HighestLabelAnchor = titleLabelAnchor; + LowestLabelAnchor = nameLabelAnchor; + } + } + + internal NameplateAnchor? GetAnchor(NameplateLabelAnchor label, bool prioritizeHealthBar) + { + if (prioritizeHealthBar && BarAnchor != null) { return BarAnchor; } + + NameplateAnchor? labelAnchor = null; + + switch (label) + { + case NameplateLabelAnchor.Name: labelAnchor = NameLabelAnchor; break; + case NameplateLabelAnchor.Title: labelAnchor = TitleLabelAnchor; break; + case NameplateLabelAnchor.Highest: labelAnchor = HighestLabelAnchor; break; + case NameplateLabelAnchor.Lowest: labelAnchor = LowestLabelAnchor; break; + } + + return labelAnchor ?? DefaultLabelAnchor; + } + #endregion + } +} diff --git a/Interface/Nameplates/NameplateConfig.cs b/Interface/Nameplates/NameplateConfig.cs new file mode 100644 index 0000000..6338534 --- /dev/null +++ b/Interface/Nameplates/NameplateConfig.cs @@ -0,0 +1,788 @@ +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Enums; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using HSUI.Interface.StatusEffects; +using System; +using System.Numerics; + +namespace HSUI.Interface.GeneralElements +{ + public enum NameplatesOcclusionMode + { + None = 0, + Simple = 1, + Full + }; + + public enum NameplatesOcclusionType + { + Walls = 0, + WallsAndObjects = 1 + }; + + [DisableParentSettings("Strata", "Position")] + [Section("Nameplates")] + [SubSection("General", 0)] + public class NameplatesGeneralConfig : MovablePluginConfigObject + { + public new static NameplatesGeneralConfig DefaultConfig() => new NameplatesGeneralConfig(); + + [Combo("Occlusion Mode", new string[] { "Disabled", "Simple", "Full" }, help = "This controls wheter you'll see nameplates through walls and objects.\n\nDisabled: Nameplates will always be seen for units in range.\nSimple: Uses simple calculations to check if a nameplate is being covered by walls or objects. Use this for better performance.\nFull: Uses more complex calculations to check if a nameplate is being covered by walls or objects. Use this for better results.")] + [Order(10)] + public NameplatesOcclusionMode OcclusionMode = NameplatesOcclusionMode.Full; + + [Combo("Occlusion Type", new string[] { "Walls", "Walls and Objects" }, help = "This controls which kind of objects will cover nameplates.\n\n\nWalls: Default setting. Only walls will cover nameplates.\n\nWalls and Objects: Some objects like columns and trees will also cover nameplates.\nThis Occlusion Type can yield some unexpected results like nameplates for NPCs behind counters not being visible.")] + [Order(11)] + public NameplatesOcclusionType OcclusionType = NameplatesOcclusionType.Walls; + + [Checkbox("Try to keep nameplates on screen", spacing = true, help = "Disclaimer: HSUI relies heavily on the the game's default nameplates so this setting won't be a huge improvement.\nThis setting tries to prevent nameplates from being cutoff in the border of the screen, but it won't keep showing nameplates that the game wouldn't.")] + [Order(20)] + public bool ClampToScreen = true; + + [Checkbox("Always show nameplate for target")] + [Order(21)] + public bool AlwaysShowTargetNameplate = true; + + public int RaycastFlag() => OcclusionType == NameplatesOcclusionType.WallsAndObjects ? 0x2000 : 0x4000; + } + + [DisableParentSettings("HideWhenInactive")] + [Section("Nameplates")] + [SubSection("Player", 0)] + public class PlayerNameplateConfig : NameplateWithPlayerBarConfig + { + public PlayerNameplateConfig( + Vector2 position, + EditableLabelConfig nameLabel, + EditableNonFormattableLabelConfig titleLabelConfig, + NameplatePlayerBarConfig barConfig) + : base(position, nameLabel, titleLabelConfig, barConfig) + { + } + + public new static PlayerNameplateConfig DefaultConfig() + { + return NameplatesHelper.GetNameplateWithBarConfig( + 0xFFD0E5E0, + 0xFF30444A, + HUDConstants.DefaultPlayerNameplateBarSize + ); + } + } + + [DisableParentSettings("HideWhenInactive", "TitleLabelConfig", "SwapLabelsWhenNeeded")] + [Section("Nameplates")] + [SubSection("Enemies", 0)] + public class EnemyNameplateConfig : NameplateWithEnemyBarConfig + { + public EnemyNameplateConfig( + Vector2 position, + EditableLabelConfig nameLabel, + EditableNonFormattableLabelConfig titleLabelConfig, + NameplateEnemyBarConfig barConfig) + : base(position, nameLabel, titleLabelConfig, barConfig) + { + } + + public new static EnemyNameplateConfig DefaultConfig() + { + EnemyNameplateConfig config = NameplatesHelper.GetNameplateWithBarConfig( + 0xFF993535, + 0xFF000000, + HUDConstants.DefaultEnemyNameplateBarSize + ); + + config.SwapLabelsWhenNeeded = false; + + config.NameLabelConfig.Position = new Vector2(-8, 0); + config.NameLabelConfig.Text = "Lv[level] [name]"; + config.NameLabelConfig.FrameAnchor = DrawAnchor.TopRight; + config.NameLabelConfig.TextAnchor = DrawAnchor.Right; + config.NameLabelConfig.Color = PluginConfigColor.FromHex(0xFFFFFFFF); + + config.BarConfig.LeftLabelConfig.Enabled = true; + config.BarConfig.OnlyShowWhenNotFull = false; + + // debuffs + LabelConfig durationConfig = new LabelConfig(new Vector2(0, -4), "", DrawAnchor.Bottom, DrawAnchor.Center); + durationConfig.FontID = FontsConfig.DefaultMediumFontKey; + + LabelConfig stacksConfig = new LabelConfig(new Vector2(-3, 4), "", DrawAnchor.TopRight, DrawAnchor.Center); + durationConfig.FontID = FontsConfig.DefaultMediumFontKey; + stacksConfig.Color = new(Vector4.UnitW); + stacksConfig.OutlineColor = new(Vector4.One); + + StatusEffectIconConfig iconConfig = new StatusEffectIconConfig(durationConfig, stacksConfig); + iconConfig.Size = new Vector2(30, 30); + iconConfig.DispellableBorderConfig.Enabled = false; + + Vector2 pos = new Vector2(2, -20); + Vector2 size = new Vector2(230, 70); + + EnemyNameplateStatusEffectsListConfig debuffs = new EnemyNameplateStatusEffectsListConfig( + DrawAnchor.TopLeft, + pos, + size, + false, + true, + false, + GrowthDirections.Right | GrowthDirections.Up, + iconConfig + ); + debuffs.Limit = 7; + debuffs.ShowPermanentEffects = true; + debuffs.IconConfig.DispellableBorderConfig.Enabled = false; + debuffs.IconPadding = new Vector2(1, 6); + debuffs.ShowOnlyMine = true; + debuffs.ShowTooltips = false; + debuffs.DisableInteraction = true; + config.DebuffsConfig = debuffs; + + // castbar + Vector2 castbarSize = new Vector2(config.BarConfig.Size.X, 10); + + LabelConfig castNameConfig = new LabelConfig(new Vector2(0, -1), "", DrawAnchor.Center, DrawAnchor.Center); + castNameConfig.FontID = FontsConfig.DefaultSmallFontKey; + + NumericLabelConfig castTimeConfig = new NumericLabelConfig(new Vector2(-5, 0), "", DrawAnchor.Right, DrawAnchor.Right); + castTimeConfig.Enabled = false; + castTimeConfig.FontID = FontsConfig.DefaultSmallFontKey; + castTimeConfig.NumberFormat = 1; + + NameplateCastbarConfig castbarConfig = new NameplateCastbarConfig(Vector2.Zero, castbarSize, castNameConfig, castTimeConfig); + castbarConfig.HealthBarAnchor = DrawAnchor.BottomLeft; + castbarConfig.Anchor = DrawAnchor.TopLeft; + castbarConfig.ShowIcon = false; + config.CastbarConfig = castbarConfig; + + return config; + } + } + + [DisableParentSettings("HideWhenInactive")] + [Section("Nameplates")] + [SubSection("Party Members", 0)] + public class PartyMembersNameplateConfig : NameplateWithPlayerBarConfig + { + public PartyMembersNameplateConfig( + Vector2 position, + EditableLabelConfig nameLabel, + EditableNonFormattableLabelConfig titleLabelConfig, + NameplatePlayerBarConfig barConfig) + : base(position, nameLabel, titleLabelConfig, barConfig) + { + } + + public new static PartyMembersNameplateConfig DefaultConfig() + { + PartyMembersNameplateConfig config = NameplatesHelper.GetNameplateWithBarConfig( + 0xFFD0E5E0, + 0xFF000000, + HUDConstants.DefaultPlayerNameplateBarSize + ); + + config.BarConfig.UseRoleColor = true; + config.NameLabelConfig.UseRoleColor = true; + config.TitleLabelConfig.UseRoleColor = true; + return config; + } + } + + [DisableParentSettings("HideWhenInactive")] + [Section("Nameplates")] + [SubSection("Alliance Members", 0)] + public class AllianceMembersNameplateConfig : NameplateWithPlayerBarConfig + { + public AllianceMembersNameplateConfig( + Vector2 position, + EditableLabelConfig nameLabel, + EditableNonFormattableLabelConfig titleLabelConfig, + NameplatePlayerBarConfig barConfig) + : base(position, nameLabel, titleLabelConfig, barConfig) + { + } + + public new static AllianceMembersNameplateConfig DefaultConfig() + { + return NameplatesHelper.GetNameplateWithBarConfig( + 0xFF99BE46, + 0xFF3D4C1C, + HUDConstants.DefaultPlayerNameplateBarSize + ); + } + } + + [DisableParentSettings("HideWhenInactive")] + [Section("Nameplates")] + [SubSection("Friends", 0)] + public class FriendPlayerNameplateConfig : NameplateWithPlayerBarConfig + { + public FriendPlayerNameplateConfig( + Vector2 position, + EditableLabelConfig nameLabel, + EditableNonFormattableLabelConfig titleLabelConfig, + NameplatePlayerBarConfig barConfig) + : base(position, nameLabel, titleLabelConfig, barConfig) + { + } + + public new static FriendPlayerNameplateConfig DefaultConfig() + { + return NameplatesHelper.GetNameplateWithBarConfig( + 0xFFEB6211, + 0xFF4A2008, + HUDConstants.DefaultPlayerNameplateBarSize + ); + } + } + + [DisableParentSettings("HideWhenInactive")] + [Section("Nameplates")] + [SubSection("Other Players", 0)] + public class OtherPlayerNameplateConfig : NameplateWithPlayerBarConfig + { + public OtherPlayerNameplateConfig( + Vector2 position, + EditableLabelConfig nameLabel, + EditableNonFormattableLabelConfig titleLabelConfig, + NameplatePlayerBarConfig barConfig) + : base(position, nameLabel, titleLabelConfig, barConfig) + { + } + + public new static OtherPlayerNameplateConfig DefaultConfig() + { + return NameplatesHelper.GetNameplateWithBarConfig( + 0xFF91BBD8, + 0xFF33434E, + HUDConstants.DefaultPlayerNameplateBarSize + ); + } + } + + [DisableParentSettings("HideWhenInactive")] + [Section("Nameplates")] + [SubSection("Pets", 0)] + public class PetNameplateConfig : NameplateWithNPCBarConfig + { + public PetNameplateConfig( + Vector2 position, + EditableLabelConfig nameLabel, + EditableNonFormattableLabelConfig titleLabelConfig, + NameplateBarConfig barConfig) + : base(position, nameLabel, titleLabelConfig, barConfig) + { + } + + public new static PetNameplateConfig DefaultConfig() + { + PetNameplateConfig config = NameplatesHelper.GetNameplateWithBarConfig( + 0xFFD1E5C8, + 0xFF2A2F28, + HUDConstants.DefaultPlayerNameplateBarSize + ); + config.OnlyShowWhenTargeted = true; + config.SwapLabelsWhenNeeded = false; + config.NameLabelConfig.Text = "Lv[level] [name]"; + config.NameLabelConfig.FontID = FontsConfig.DefaultSmallFontKey; + config.TitleLabelConfig.FontID = FontsConfig.DefaultSmallFontKey; + + return config; + } + } + + [DisableParentSettings("HideWhenInactive")] + [Section("Nameplates")] + [SubSection("NPCs", 0)] + public class NPCNameplateConfig : NameplateWithNPCBarConfig + { + public NPCNameplateConfig( + Vector2 position, + EditableLabelConfig nameLabel, + EditableNonFormattableLabelConfig titleLabelConfig, + NameplateBarConfig barConfig) + : base(position, nameLabel, titleLabelConfig, barConfig) + { + } + + public new static NPCNameplateConfig DefaultConfig() + { + NPCNameplateConfig config = NameplatesHelper.GetNameplateWithBarConfig( + 0xFFD1E5C8, + 0xFF3A4b1E, + HUDConstants.DefaultPlayerNameplateBarSize + ); + config.NameLabelConfig.Position = new Vector2(0, -20); + config.TitleLabelConfig.Position = Vector2.Zero; + + return config; + } + } + + [DisableParentSettings("HideWhenInactive", "SwapLabelsWhenNeeded")] + [Section("Nameplates")] + [SubSection("Minions", 0)] + public class MinionNPCNameplateConfig : NameplateConfig + { + public MinionNPCNameplateConfig( + Vector2 position, + EditableLabelConfig nameLabel, + EditableNonFormattableLabelConfig titleLabelConfig) + : base(position, nameLabel, titleLabelConfig) + { + } + + public new static MinionNPCNameplateConfig DefaultConfig() + { + MinionNPCNameplateConfig config = NameplatesHelper.GetNameplateConfig(0xFFFFFFFF, 0xFF000000); + config.OnlyShowWhenTargeted = true; + config.SwapLabelsWhenNeeded = false; + config.NameLabelConfig.Position = new Vector2(0, -17); + config.NameLabelConfig.FontID = FontsConfig.DefaultSmallFontKey; + config.TitleLabelConfig.Position = new Vector2(0, 0); + config.TitleLabelConfig.FontID = FontsConfig.DefaultSmallFontKey; + + return config; + } + } + + [DisableParentSettings("HideWhenInactive", "SwapLabelsWhenNeeded")] + [Section("Nameplates")] + [SubSection("Objects", 0)] + public class ObjectsNameplateConfig : NameplateConfig + { + public ObjectsNameplateConfig( + Vector2 position, + EditableLabelConfig nameLabel, + EditableNonFormattableLabelConfig titleLabelConfig) + : base(position, nameLabel, titleLabelConfig) + { + } + + public new static ObjectsNameplateConfig DefaultConfig() + { + ObjectsNameplateConfig config = NameplatesHelper.GetNameplateConfig(0xFFFFFFFF, 0xFF000000); + config.SwapLabelsWhenNeeded = false; + + return config; + } + } + + public class NameplateConfig : MovablePluginConfigObject + { + [Checkbox("Only show when targeted")] + [Order(1)] + public bool OnlyShowWhenTargeted = false; + + [Checkbox("Swap Name and Title labels when needed", spacing = true, help = "This will swap the contents of these labels depending on if the title goes before or after the name of a player.")] + [Order(20)] + public bool SwapLabelsWhenNeeded = true; + + [NestedConfig("Name Label", 21)] + public EditableLabelConfig NameLabelConfig = null!; + + [NestedConfig("Title Label", 22)] + public EditableNonFormattableLabelConfig TitleLabelConfig = null!; + + [NestedConfig("Change Alpha Based on Range", 145)] + public NameplateRangeConfig RangeConfig = new(); + + [NestedConfig("Visibility", 200)] + public VisibilityConfig VisibilityConfig = new VisibilityConfig(); + + public NameplateConfig(Vector2 position, EditableLabelConfig nameLabelConfig, EditableNonFormattableLabelConfig titleLabelConfig) + : base() + { + Position = position; + NameLabelConfig = nameLabelConfig; + TitleLabelConfig = titleLabelConfig; + } + + public NameplateConfig() : base() { } // don't remove + } + + public interface NameplateWithBarConfig + { + public NameplateBarConfig GetBarConfig(); + } + + public class NameplateWithNPCBarConfig : NameplateConfig, NameplateWithBarConfig + { + [NestedConfig("Health Bar", 40)] + public NameplateBarConfig BarConfig = null!; + + public NameplateBarConfig GetBarConfig() => BarConfig; + + public NameplateWithNPCBarConfig( + Vector2 position, + EditableLabelConfig nameLabel, + EditableNonFormattableLabelConfig titleLabelConfig, + NameplateBarConfig barConfig) + : base(position, nameLabel, titleLabelConfig) + { + BarConfig = barConfig; + } + + public NameplateWithNPCBarConfig() : base() { } // don't remove + } + + public class NameplateWithPlayerBarConfig : NameplateConfig, NameplateWithBarConfig + { + [NestedConfig("Health Bar", 40)] + public NameplatePlayerBarConfig BarConfig = null!; + + [NestedConfig("Role/Job Icon", 50)] + public NameplateRoleJobIconConfig RoleIconConfig = new NameplateRoleJobIconConfig( + new Vector2(-5, 0), + new Vector2(30, 30), + DrawAnchor.Right, + DrawAnchor.Left + ) + { Strata = StrataLevel.LOWEST }; + + [NestedConfig("Player State Icon", 55)] + public NameplatePlayerIconConfig StateIconConfig = new NameplatePlayerIconConfig( + new Vector2(5, 0), + new Vector2(30, 30), + DrawAnchor.Left, + DrawAnchor.Right + ) + { Strata = StrataLevel.LOWEST }; + + public NameplateBarConfig GetBarConfig() => BarConfig; + + public NameplateWithPlayerBarConfig( + Vector2 position, + EditableLabelConfig nameLabel, + EditableNonFormattableLabelConfig titleLabelConfig, + NameplatePlayerBarConfig barConfig) + : base(position, nameLabel, titleLabelConfig) + { + BarConfig = barConfig; + } + + public NameplateWithPlayerBarConfig() : base() { } // don't remove + } + + public class NameplateWithEnemyBarConfig : NameplateConfig, NameplateWithBarConfig + { + [NestedConfig("Health Bar", 40)] + public NameplateEnemyBarConfig BarConfig = null!; + + [NestedConfig("Icon", 45)] + public NameplateIconConfig IconConfig = new NameplateIconConfig( + new Vector2(0, 0), + new Vector2(40, 40), + DrawAnchor.Right, + DrawAnchor.Left + ) + { PrioritizeHealthBarAnchor = true, Strata = StrataLevel.LOWEST }; + + [NestedConfig("Debuffs", 50)] + public EnemyNameplateStatusEffectsListConfig DebuffsConfig = null!; + + [NestedConfig("Castbar", 55)] + public NameplateCastbarConfig CastbarConfig = null!; + + public NameplateBarConfig GetBarConfig() => BarConfig; + + public NameplateWithEnemyBarConfig( + Vector2 position, + EditableLabelConfig nameLabel, + EditableNonFormattableLabelConfig titleLabelConfig, + NameplateEnemyBarConfig barConfig) + : base(position, nameLabel, titleLabelConfig) + { + BarConfig = barConfig; + } + + public NameplateWithEnemyBarConfig() : base() { } // don't remove + } + + [DisableParentSettings("HideWhenInactive")] + public class NameplateBarConfig : BarConfig + { + [Checkbox("Only Show when not at full Health")] + [Order(1)] + public bool OnlyShowWhenNotFull = true; + + [Checkbox("Hide Health when fully depleted", help = "This will hide the healthbar when the characters HP has been brought to zero")] + [Order(2)] + public bool HideHealthAtZero = true; + + [Checkbox("Disable Interaction")] + [Order(3)] + public bool DisableInteraction = false; + + [Checkbox("Use Different Size when targeted", spacing = true)] + [Order(31)] + public bool UseDifferentSizeWhenTargeted = false; + + [DragInt2("Size When Targeted", min = 1, max = 4000)] + [Order(32, collapseWith = nameof(UseDifferentSizeWhenTargeted))] + public Vector2 SizeWhenTargeted; + + [ColorEdit4("Targeted Border Color")] + [Order(38, collapseWith = nameof(DrawBorder))] + public PluginConfigColor TargetedBorderColor = PluginConfigColor.FromHex(0xFFFFFFFF); + + [DragInt("Targeted Border Thickness", min = 1, max = 10)] + [Order(39, collapseWith = nameof(DrawBorder))] + public int TargetedBorderThickness = 2; + + [NestedConfig("Color Based On Health Value", 50, collapsingHeader = false)] + public ColorByHealthValueConfig ColorByHealth = new ColorByHealthValueConfig(); + + [Checkbox("Hide Health if Possible", spacing = true, help = "This will hide any label that has a health tag if the character doesn't have health (ie minions, friendly npcs, etc)")] + [Order(121)] + public bool HideHealthIfPossible = true; + + [NestedConfig("Left Text", 125)] + public EditableLabelConfig LeftLabelConfig = null!; + + [NestedConfig("Right Text", 130)] + public EditableLabelConfig RightLabelConfig = null!; + + [NestedConfig("Optional Text", 131)] + public EditableLabelConfig OptionalLabelConfig = null!; + + [NestedConfig("Shields", 140)] + public ShieldConfig ShieldConfig = new ShieldConfig(); + + [NestedConfig("Custom Mouseover Area", 150)] + public MouseoverAreaConfig MouseoverAreaConfig = new MouseoverAreaConfig(); + + public NameplateBarConfig(Vector2 position, Vector2 size, EditableLabelConfig leftLabelConfig, EditableLabelConfig rightLabelConfig, EditableLabelConfig optionalLabelConfig) + : base(position, size, new PluginConfigColor(new(40f / 255f, 40f / 255f, 40f / 255f, 100f / 100f))) + { + Position = position; + Size = size; + LeftLabelConfig = leftLabelConfig; + RightLabelConfig = rightLabelConfig; + OptionalLabelConfig = optionalLabelConfig; + BackgroundColor = new PluginConfigColor(new(0f / 255f, 0f / 255f, 0f / 255f, 100f / 100f)); + ColorByHealth.Enabled = false; + MouseoverAreaConfig.Enabled = false; + } + + public bool IsVisible(uint hp, uint maxHp) + { + return Enabled && (!OnlyShowWhenNotFull || hp < maxHp) && !(HideHealthAtZero && hp <= 0); + } + + public Vector2 GetSize(bool targeted) + { + return targeted && UseDifferentSizeWhenTargeted ? SizeWhenTargeted : Size; + } + + public NameplateBarConfig() : base(Vector2.Zero, Vector2.Zero, PluginConfigColor.Empty) { } // don't remove + } + + public class NameplatePlayerBarConfig : NameplateBarConfig + { + [Checkbox("Use Job Color", spacing = true)] + [Order(45)] + public bool UseJobColor = false; + + [Checkbox("Use Role Color")] + [Order(46)] + public bool UseRoleColor = false; + + [Checkbox("Job Color As Background Color", spacing = true)] + [Order(50)] + public bool UseJobColorAsBackgroundColor = false; + + [Checkbox("Role Color As Background Color")] + [Order(51)] + public bool UseRoleColorAsBackgroundColor = false; + + public NameplatePlayerBarConfig(Vector2 position, Vector2 size, EditableLabelConfig leftLabelConfig, EditableLabelConfig rightLabelConfig, EditableLabelConfig optionalLabelConfig) + : base(position, size, leftLabelConfig, rightLabelConfig, optionalLabelConfig) + { + } + } + + public class NameplateEnemyBarConfig : NameplateBarConfig + { + [Checkbox("Use State Colors", spacing = true)] + [Order(45)] + public bool UseStateColor = true; + + [ColorEdit4("Unengaged")] + [Order(46, collapseWith = nameof(UseStateColor))] + public PluginConfigColor UnengagedColor = PluginConfigColor.FromHex(0xFFDA9D2E); + + [ColorEdit4("Unengaged (Hostile)")] + [Order(47, collapseWith = nameof(UseStateColor))] + public PluginConfigColor UnengagedHostileColor = PluginConfigColor.FromHex(0xFF994B35); + + [ColorEdit4("Engaged")] + [Order(48, collapseWith = nameof(UseStateColor))] + public PluginConfigColor EngagedColor = PluginConfigColor.FromHex(0xFF993535); + + [ColorEdit4("Claimed")] + [Order(49, collapseWith = nameof(UseStateColor))] + public PluginConfigColor ClaimedColor = PluginConfigColor.FromHex(0xFFEA93EA); + + [ColorEdit4("Unclaimed")] + [Order(50, collapseWith = nameof(UseStateColor))] + public PluginConfigColor UnclaimedColor = PluginConfigColor.FromHex(0xFFE5BB9E); + + [Checkbox("Use Custom Color when being targeted", spacing = true, help = "This will change the color of the bar when the enemy is targeting the player.")] + [Order(51)] + public bool UseCustomColorWhenBeingTargeted = false; + + [ColorEdit4("Targeted")] + [Order(52, collapseWith = nameof(UseCustomColorWhenBeingTargeted))] + public PluginConfigColor CustomColorWhenBeingTargeted = PluginConfigColor.FromHex(0xFFC4216D); + + [NestedConfig("Order Label", 132)] + public DefaultFontLabelConfig OrderLabelConfig = new DefaultFontLabelConfig(new Vector2(5, 0), "", DrawAnchor.Right, DrawAnchor.Left) + { + Strata = StrataLevel.LOWEST + }; + + public NameplateEnemyBarConfig(Vector2 position, Vector2 size, EditableLabelConfig leftLabelConfig, EditableLabelConfig rightLabelConfig, EditableLabelConfig optionalLabelConfig) + : base(position, size, leftLabelConfig, rightLabelConfig, optionalLabelConfig) + { + + } + } + + [Exportable(false)] + public class NameplateRangeConfig : PluginConfigObject + { + [DragInt("Fade start range (yalms)", min = 1, max = 500)] + [Order(5)] + public int StartRange = 50; + + [DragInt("Fade end range (yalms)", min = 1, max = 500)] + [Order(10)] + public int EndRange = 64; + + public float AlphaForDistance(float distance, float maxAlpha = 1f) + { + float diff = distance - StartRange; + if (!Enabled || diff <= 0) + { + return maxAlpha; + } + + float a = diff / (EndRange - StartRange); + return Math.Max(0, Math.Min(maxAlpha, 1 - a)); + } + } + + public class EnemyNameplateStatusEffectsListConfig : StatusEffectsListConfig + { + [Anchor("Health Bar Anchor")] + [Order(4)] + public DrawAnchor HealthBarAnchor = DrawAnchor.BottomLeft; + + public EnemyNameplateStatusEffectsListConfig(DrawAnchor anchor, Vector2 position, Vector2 size, bool showBuffs, bool showDebuffs, bool showPermanentEffects, + GrowthDirections growthDirections, StatusEffectIconConfig iconConfig) + : base(position, size, showBuffs, showDebuffs, showPermanentEffects, growthDirections, iconConfig) + { + HealthBarAnchor = anchor; + } + } + + [DisableParentSettings("AnchorToUnitFrame", "UnitFrameAnchor", "HideWhenInactive", "FillDirection")] + public class NameplateCastbarConfig : TargetCastbarConfig + { + [Checkbox("Match Width with Health Bar")] + [Order(11)] + public bool MatchWidth = false; + + [Checkbox("Match Height with Health Bar")] + [Order(12)] + public bool MatchHeight = false; + + [Anchor("Health Bar Anchor")] + [Order(16)] + public DrawAnchor HealthBarAnchor = DrawAnchor.BottomLeft; + + public NameplateCastbarConfig(Vector2 position, Vector2 size, LabelConfig castNameConfig, NumericLabelConfig castTimeConfig) + : base(position, size, castNameConfig, castTimeConfig) + { + + } + } + + internal static class NameplatesHelper + { + internal static T GetNameplateConfig(uint bgColor, uint borderColor) where T : NameplateConfig + { + EditableLabelConfig nameLabelConfig = new EditableLabelConfig(new Vector2(0, 0), "[name]", DrawAnchor.Top, DrawAnchor.Bottom) + { + Color = PluginConfigColor.FromHex(bgColor), + OutlineColor = PluginConfigColor.FromHex(borderColor), + FontID = FontsConfig.DefaultMediumFontKey + }; + + EditableNonFormattableLabelConfig titleLabelConfig = new EditableNonFormattableLabelConfig(new Vector2(0, -25), "<[title]>", DrawAnchor.Top, DrawAnchor.Bottom) + { + Color = PluginConfigColor.FromHex(bgColor), + OutlineColor = PluginConfigColor.FromHex(borderColor), + FontID = FontsConfig.DefaultMediumFontKey + }; + + return (T)Activator.CreateInstance(typeof(T), Vector2.Zero, nameLabelConfig, titleLabelConfig)!; + } + + internal static T GetNameplateWithBarConfig(uint bgColor, uint borderColor, Vector2 barSize) + where T : NameplateConfig + where B : NameplateBarConfig + { + EditableLabelConfig leftLabelConfig = new EditableLabelConfig(new Vector2(5, 0), "[health:current-short]", DrawAnchor.Left, DrawAnchor.Left) + { + Enabled = false, + FontID = FontsConfig.DefaultMediumFontKey, + Strata = StrataLevel.LOWEST + }; + EditableLabelConfig rightLabelConfig = new EditableLabelConfig(new Vector2(-5, 0), "", DrawAnchor.Right, DrawAnchor.Right) + { + Enabled = false, + FontID = FontsConfig.DefaultMediumFontKey, + Strata = StrataLevel.LOWEST + }; + EditableLabelConfig optionalLabelConfig = new EditableLabelConfig(new Vector2(0, 0), "", DrawAnchor.Center, DrawAnchor.Center) + { + Enabled = false, + FontID = FontsConfig.DefaultSmallFontKey, + Strata = StrataLevel.LOWEST + }; + + var barConfig = Activator.CreateInstance(typeof(B), new Vector2(0, -5), barSize, leftLabelConfig, rightLabelConfig, optionalLabelConfig)!; + if (barConfig is BarConfig bar) + { + bar.FillColor = PluginConfigColor.FromHex(bgColor); + bar.BackgroundColor = PluginConfigColor.FromHex(0xAA000000); + } + + if (barConfig is NameplateBarConfig nameplateBar) + { + nameplateBar.SizeWhenTargeted = nameplateBar.Size; + } + + EditableLabelConfig nameLabelConfig = new EditableLabelConfig(new Vector2(0, -20), "[name]", DrawAnchor.Top, DrawAnchor.Bottom) + { + Color = PluginConfigColor.FromHex(bgColor), + OutlineColor = PluginConfigColor.FromHex(borderColor), + FontID = FontsConfig.DefaultMediumFontKey, + Strata = StrataLevel.LOWEST + }; + EditableNonFormattableLabelConfig titleLabelConfig = new EditableNonFormattableLabelConfig(new Vector2(0, 0), "<[title]>", DrawAnchor.Top, DrawAnchor.Bottom) + { + Color = PluginConfigColor.FromHex(bgColor), + OutlineColor = PluginConfigColor.FromHex(borderColor), + FontID = FontsConfig.DefaultMediumFontKey, + Strata = StrataLevel.LOWEST + }; + + return (T)Activator.CreateInstance(typeof(T), Vector2.Zero, nameLabelConfig, titleLabelConfig, barConfig)!; + } + } +} diff --git a/Interface/Nameplates/NameplatesHud.cs b/Interface/Nameplates/NameplatesHud.cs new file mode 100644 index 0000000..5db44fd --- /dev/null +++ b/Interface/Nameplates/NameplatesHud.cs @@ -0,0 +1,232 @@ +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.ClientState.Objects.Types; +using HSUI.Config; +using HSUI.Helpers; +using HSUI.Interface.GeneralElements; +using FFXIVClientStructs.FFXIV.Client.Game.Control; +using FFXIVClientStructs.FFXIV.Client.Graphics; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.System.Framework; +using FFXIVClientStructs.FFXIV.Common.Component.BGCollision; +using System.Numerics; + +namespace HSUI.Interface.Nameplates +{ + internal class NameplatesHud : HudElement + { + private NameplatesGeneralConfig Config => (NameplatesGeneralConfig)_config; + + private NameplateWithPlayerBar _playerHud; + private NameplateWithEnemyBar _enemyHud; + private NameplateWithPlayerBar _partyMemberHud; + private NameplateWithPlayerBar _allianceMemberHud; + private NameplateWithPlayerBar _friendsHud; + private NameplateWithPlayerBar _otherPlayersHud; + private NameplateWithBar _petHud; + private NameplateWithBar _npcHud; + private Nameplate _minionNPCHud; + private Nameplate _objectHud; + + private bool _wasHovering; + + public NameplatesHud(NameplatesGeneralConfig config) : base(config) + { + ConfigurationManager manager = ConfigurationManager.Instance; + _playerHud = new NameplateWithPlayerBar(manager.GetConfigObject()); + _enemyHud = new NameplateWithEnemyBar(manager.GetConfigObject()); + _partyMemberHud = new NameplateWithPlayerBar(manager.GetConfigObject()); + _allianceMemberHud = new NameplateWithPlayerBar(manager.GetConfigObject()); + _friendsHud = new NameplateWithPlayerBar(manager.GetConfigObject()); + _otherPlayersHud = new NameplateWithPlayerBar(manager.GetConfigObject()); + _petHud = new NameplateWithBar(manager.GetConfigObject()); + _npcHud = new NameplateWithBar(manager.GetConfigObject()); + _minionNPCHud = new Nameplate(manager.GetConfigObject()); + _objectHud = new Nameplate(manager.GetConfigObject()); + } + + public void StopPreview() + { + _enemyHud.StopPreview(); + } + + public void StopMouseover() + { + if (_wasHovering) + { + InputsHelper.Instance.ClearTarget(); + _wasHovering = false; + } + } + + protected override void CreateDrawActions(Vector2 origin) + { + if (!_config.Enabled || NameplatesManager.Instance == null) + { + StopMouseover(); + return; + } + + IGameObject? mouseoveredActor = null; + bool ignoreMouseover = false; + + foreach (NameplateData data in NameplatesManager.Instance.Data) + { + Nameplate? nameplate = GetNameplate(data); + if (nameplate == null || !nameplate.Enabled) { continue; } + + // raycasting + if (IsPointObstructed(data)) { continue; } + + if (nameplate is NameplateWithBar nameplateWithBar) + { + // draw bar + AddDrawActions(nameplateWithBar.GetBarDrawActions(data)); + + // find mouseovered nameplate + var (isHovering, ignore) = nameplateWithBar.GetMouseoverState(data); + if (isHovering) + { + mouseoveredActor = data.GameObject; + ignoreMouseover = ignore; + } + } + + // draw elements + AddDrawActions(nameplate.GetElementsDrawActions(data)); + } + + // mouseover + if (mouseoveredActor != null) + { + _wasHovering = true; + InputsHelper.Instance.SetTarget(mouseoveredActor, ignoreMouseover); + + if (InputsHelper.Instance.LeftButtonClicked) + { + Plugin.TargetManager.Target = mouseoveredActor; + InputsHelper.Instance.ClearClicks(); + } + else if (InputsHelper.Instance.RightButtonClicked) + { + InputsHelper.Instance.ClearClicks(); + } + } + else if (_wasHovering) + { + InputsHelper.Instance.ClearTarget(); + _wasHovering = false; + } + } + + private unsafe Nameplate? GetNameplate(NameplateData data) + { + switch (data.Kind) + { + case ObjectKind.Player: + if (data.GameObject?.EntityId == Plugin.ObjectTable.LocalPlayer?.EntityId) + { + return _playerHud; + } + + if (data.GameObject is ICharacter character) + { + if ((character.StatusFlags & StatusFlags.PartyMember) != 0) // PartyMember + { + return _partyMemberHud; + } + else if ((character.StatusFlags & StatusFlags.AllianceMember) != 0) // AllianceMember + { + return _allianceMemberHud; + } + else if ((character.StatusFlags & StatusFlags.Friend) != 0) // Friend + { + return _friendsHud; + } + } + + return _otherPlayersHud; + + case ObjectKind.BattleNpc: + if (data.GameObject is IBattleNpc battleNpc) + { + if ((BattleNpcSubKind)battleNpc.SubKind == BattleNpcSubKind.Pet || + (BattleNpcSubKind)battleNpc.SubKind == BattleNpcSubKind.Chocobo) + { + return _petHud; + } + else if ((BattleNpcSubKind)battleNpc.SubKind == BattleNpcSubKind.Enemy || + (BattleNpcSubKind)battleNpc.SubKind == BattleNpcSubKind.BattleNpcPart) + { + return Utils.IsHostile(data.GameObject) ? _enemyHud : _npcHud; + } + else if (battleNpc.SubKind == 10) // island released minions + { + return _npcHud; + } + } + break; + + case ObjectKind.EventNpc: return _npcHud; + case ObjectKind.Companion: return _minionNPCHud; + default: return _objectHud; + } + + return null; + } + + private unsafe bool IsPointObstructed(NameplateData data) + { + if (data.GameObject == null) { return true; } + if (Config.OcclusionMode == NameplatesOcclusionMode.None || data.IgnoreOcclusion) { return false; } + + Camera camera = Control.Instance()->CameraManager.Camera->CameraBase.SceneCamera; + Vector3 cameraPos = camera.Object.Position; + + BGCollisionModule* collisionModule = Framework.Instance()->BGCollisionModule; + if (collisionModule == null) + { + return false; + } + + int flag = Config.RaycastFlag(); + int* flags = stackalloc int[] { flag, 0, flag, 0 }; + bool obstructed = false; + + // simple mode + if (Config.OcclusionMode == NameplatesOcclusionMode.Simple) + { + Vector3 direction = Vector3.Normalize(data.WorldPosition - cameraPos); + RaycastHit hit; + obstructed = collisionModule->RaycastMaterialFilter(&hit, &cameraPos, &direction, data.Distance, 1, flags); + } + // full mode + else + { + int obstructionCount = 0; + Vector2[] points = new Vector2[] + { + data.ScreenPosition + new Vector2(-30, 0), // left + data.ScreenPosition + new Vector2(30, 0), // right + }; + + foreach (Vector2 point in points) + { + Ray ray = camera.ScreenPointToRay(point); + RaycastHit hit; + + Vector3 origin = new Vector3(ray.Origin.X, ray.Origin.Y, ray.Origin.Z); + Vector3 direction = new Vector3(ray.Direction.X, ray.Direction.Y, ray.Direction.Z); + + if (collisionModule->RaycastMaterialFilter(&hit, &origin, &direction, data.Distance, 1, flags)) + { + obstructionCount++; + } + } + + obstructed = obstructionCount == points.Length; + } + + return obstructed; + } + } +} diff --git a/Interface/Nameplates/NameplatesManager.cs b/Interface/Nameplates/NameplatesManager.cs new file mode 100644 index 0000000..afde57b --- /dev/null +++ b/Interface/Nameplates/NameplatesManager.cs @@ -0,0 +1,365 @@ +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Memory; +using HSUI.Config; +using HSUI.Helpers; +using HSUI.Interface.GeneralElements; +using FFXIVClientStructs.FFXIV.Client.Game.Control; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Component.GUI; +using Dalamud.Bindings.ImGui; +using System; +using System.Collections.Generic; +using System.Numerics; +using static FFXIVClientStructs.FFXIV.Client.UI.AddonNamePlate; +using static FFXIVClientStructs.FFXIV.Client.UI.RaptureAtkModule; +using static FFXIVClientStructs.FFXIV.Client.UI.UI3DModule; +using StructsFramework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework; +using StructsGameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; + +namespace HSUI.Interface.Nameplates +{ + internal class NameplatesManager : IDisposable + { + #region Singleton + public static NameplatesManager Instance { get; private set; } = null!; + private NameplatesGeneralConfig _config = null!; + + private NameplatesManager() + { + Plugin.ClientState.TerritoryChanged -= ClientStateOnTerritoryChangedEvent; + ConfigurationManager.Instance.ResetEvent += OnConfigReset; + + OnConfigReset(ConfigurationManager.Instance); + } + + public static void Initialize() + { + Instance = new NameplatesManager(); + } + + ~NameplatesManager() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + + Plugin.ClientState.TerritoryChanged -= ClientStateOnTerritoryChangedEvent; + + Instance = null!; + } + + private void OnConfigReset(ConfigurationManager sender) + { + _config = sender.GetConfigObject(); + } + #endregion Singleton + + private const int NameplateCount = 50; + private const int NameplateDataArrayIndex = 4; // TODO: Rework to use NamePlateStringArray if it exists or use AtkStage.Instance()->GetStringArrayData(StringArrayType.NamePlate) + private Vector2 _averageNameplateSize = new Vector2(250, 150); + private List _data = new List(); + public IReadOnlyCollection Data => _data.AsReadOnly(); + + private NameplatesCache _cache = new NameplatesCache(50); + + private void ClientStateOnTerritoryChangedEvent(ushort territoryId) + { + _cache.Clear(); + } + + public unsafe void Update() + { + if (!_config.Enabled) { return; } + + UIModule* uiModule = StructsFramework.Instance()->GetUIModule(); + if (uiModule == null) { return; } + + UI3DModule* ui3DModule = uiModule->GetUI3DModule(); + if (ui3DModule == null) { return; } + + AddonNamePlate* addon = (AddonNamePlate*)Plugin.GameGui.GetAddonByName("NamePlate", 1).Address; + if (addon == null) { return; } + + RaptureAtkModule* atkModule = uiModule->GetRaptureAtkModule(); + if (atkModule == null || atkModule->AtkModule.AtkArrayDataHolder.StringArrayCount <= NameplateDataArrayIndex) { return; } + + StringArrayData* stringArray = atkModule->AtkModule.AtkArrayDataHolder.StringArrays[NameplateDataArrayIndex]; + Span infoArray = atkModule->NamePlateInfoEntries; + Camera camera = Control.Instance()->CameraManager.Camera->CameraBase.SceneCamera; + + IGameObject? target = Plugin.TargetManager.Target; + bool foundTarget = false; + NameplateData? targetData = null; + + _data = new List(); + int activeCount = ui3DModule->NamePlateObjectInfoCount; + + for (int i = 0; i < activeCount; i++) + { + try + { + ObjectInfo* objectInfo = ui3DModule->NamePlateObjectInfoPointers[i]; + if (objectInfo == null || objectInfo->NamePlateIndex >= NameplateCount) { continue; } + + // actor + StructsGameObject* obj = objectInfo->GameObject; + if (obj == null) { continue; } + + bool isTarget = false; + IGameObject? gameObject = Plugin.ObjectTable.CreateObjectReference(new IntPtr(obj)); + if (target != null && new IntPtr(obj) == target.Address) + { + isTarget = true; + foundTarget = true; + } + + // ui nameplate + NamePlateObject nameplateObject = addon->NamePlateObjectArray[objectInfo->NamePlateIndex]; + + // position + Vector2 screenPos = new Vector2( + nameplateObject.RootComponentNode->AtkResNode.X + nameplateObject.RootComponentNode->AtkResNode.Width / 2f, + nameplateObject.RootComponentNode->AtkResNode.Y + nameplateObject.RootComponentNode->AtkResNode.Height + ); + screenPos = ClampScreenPosition(screenPos); + + Vector3 worldPos = new Vector3(obj->Position.X, obj->Position.Y + obj->Height * 2.2f, obj->Position.Z); + + // distance + float distance = Vector3.Distance(camera.Object.Position, worldPos); + + // name + NamePlateInfo info = infoArray[objectInfo->NamePlateIndex]; + string name = info.Name.ToString(); + + // title + string title = info.Title.ToString(); + bool isTitlePrefix = info.IsPrefixTitle; + + // Get the title from Honorific, if it exists + TitleData? customTitleData = HonorificHelper.Instance?.GetTitle(gameObject); + if (customTitleData != null) + { + title = customTitleData.Title; + isTitlePrefix = customTitleData.IsPrefix; + } + + // state icon + int iconId = 0; + AtkUldAsset* textureInfo = nameplateObject.NameIcon->PartsList->Parts[nameplateObject.NameIcon->PartId].UldAsset; + if (textureInfo != null && textureInfo->AtkTexture.Resource != null) + { + iconId = (int)textureInfo->AtkTexture.Resource->IconId; + } + + // order + int arrayIndex = 200 + (activeCount - nameplateObject.Priority - 1); + string order = ""; + try + { + if (stringArray->AtkArrayData.Size > arrayIndex && stringArray->StringArray[arrayIndex] != null) + { + order = MemoryHelper.ReadSeStringNullTerminated(new IntPtr(stringArray->StringArray[arrayIndex])).ToString(); + } + } + catch { } + + NameplateData data = new NameplateData( + gameObject, + name, + title, + isTitlePrefix, + iconId, + order, + (ObjectKind)obj->ObjectKind, + obj->SubKind, + screenPos, + worldPos, + distance + ); + + if (isTarget) + { + targetData = data; + } + else + { + _data.Add(data); + } + + _cache.Add(obj->GetGameObjectId().ObjectId, data); + } + catch { } + } + + _data.Reverse(); + + // add target nameplate last + if (foundTarget && targetData.HasValue) + { + _data.Add(targetData.Value); + } + // create nameplate for target? + else if (_config.AlwaysShowTargetNameplate && target != null && !foundTarget) + { + StructsGameObject* obj = (StructsGameObject*)target.Address; + NameplateData? cachedData = _cache[(uint)target.GameObjectId]; + + Vector3 worldPos = new Vector3(target.Position.X, target.Position.Y + obj->Height * 2.2f, target.Position.Z); + float distance = Vector3.Distance(camera.Object.Position, worldPos); + + Plugin.GameGui.WorldToScreen(worldPos, out Vector2 screenPos); + screenPos = ClampScreenPosition(screenPos); + + targetData = new NameplateData( + target, + target.Name.ToString(), + cachedData?.Title ?? "", + cachedData?.IsTitlePrefix ?? true, + cachedData?.NamePlateIconId ?? 0, + cachedData?.Order ?? "", + target.ObjectKind, + target.SubKind, + screenPos, + worldPos, + distance, + true + ); + + _data.Add(targetData.Value); + } + } + + private Vector2 ClampScreenPosition(Vector2 pos) + { + if (!_config.ClampToScreen) { return pos; } + + Vector2 screenSize = ImGui.GetMainViewport().Size; + Vector2 nameplateSize = _averageNameplateSize / 2f; + float margin = 20; + + if (pos.X + nameplateSize.X > screenSize.X) + { + pos.X = screenSize.X - nameplateSize.X - margin; + } + else if (pos.X - nameplateSize.X < 0) + { + pos.X = nameplateSize.X + margin; + } + + if (pos.Y + nameplateSize.Y > screenSize.Y) + { + pos.Y = screenSize.Y - nameplateSize.Y - margin; + } + else if (pos.Y - nameplateSize.Y < 0) + { + pos.Y = nameplateSize.Y + margin; + } + + return pos; + } + } + + #region utils + public class NameplatesCache + { + + private int _limit; + private Dictionary _dict; + private Queue _queue; + + public NameplatesCache(int limit) + { + _limit = limit; + _dict = new Dictionary(limit); + _queue = new Queue(limit); + } + + public void Add(uint key, NameplateData data) + { + if (key == 0 || key == 0xE0000000) { return; } + + if (_dict.Count == _limit) + { + uint oldestKey = _queue.Dequeue(); + _dict.Remove(oldestKey); + } + + if (_dict.ContainsKey(key)) + { + _dict[key] = data; + } + else + { + _dict.Add(key, data); + _queue.Enqueue(key); + } + } + + public void Clear() + { + _dict.Clear(); + _queue.Clear(); + } + + public NameplateData? this[uint key] + { + get + { + if (_dict.TryGetValue(key, out NameplateData data)) + { + return data; + } + + return null; + } + } + } + + public struct NameplateData + { + public IGameObject? GameObject; + public string Name; + public string Title; + public bool IsTitlePrefix; + public int NamePlateIconId; + public string Order; + public ObjectKind Kind; + public byte SubKind; + public Vector2 ScreenPosition; + public Vector3 WorldPosition; + public float Distance; + public bool IgnoreOcclusion; + + public NameplateData(IGameObject? gameObject, string name, string title, bool isTitlePrefix, int namePlateIconId, string order, ObjectKind kind, byte subKind, Vector2 screenPosition, Vector3 worldPosition, float distance, bool ignoreOcclusion = false) + { + GameObject = gameObject; + Name = name; + Title = title; + IsTitlePrefix = isTitlePrefix; + NamePlateIconId = namePlateIconId; + Order = order; + Kind = kind; + SubKind = subKind; + ScreenPosition = screenPosition; + WorldPosition = worldPosition; + Distance = distance; + IgnoreOcclusion = ignoreOcclusion; + } + } + #endregion +} diff --git a/Interface/Party/PartyFramesBar.cs b/Interface/Party/PartyFramesBar.cs new file mode 100644 index 0000000..5df07bd --- /dev/null +++ b/Interface/Party/PartyFramesBar.cs @@ -0,0 +1,748 @@ +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.Interface.Textures.TextureWraps; +using HSUI.Config; +using HSUI.Enums; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using HSUI.Interface.GeneralElements; +using HSUI.Interface.StatusEffects; +using Dalamud.Bindings.ImGui; +using System; +using System.Collections.Generic; +using System.Numerics; + +namespace HSUI.Interface.Party +{ + public class PartyFramesBar + { + public delegate void PartyFramesBarEventHandler(PartyFramesBar bar); + public PartyFramesBarEventHandler? OpenContextMenuEvent; + + private PartyFramesConfigs _configs; + + private LabelHud _nameLabelHud; + private LabelHud _healthLabelHud; + private LabelHud _orderLabelHud; + private LabelHud _statusLabelHud; + private LabelHud _raiseLabelHud; + private LabelHud _invulnLabelHud; + private PrimaryResourceHud _manaBarHud; + private CastbarHud _castbarHud; + private StatusEffectsListHud _buffsListHud; + private StatusEffectsListHud _debuffsListHud; + private PartyFramesCooldownListHud _cooldownListHud; + + private IDalamudTextureWrap? _readyCheckTexture => + TexturesHelper.GetTextureFromPath("ui/uld/ReadyCheck_hr1.tex") ?? + TexturesHelper.GetTextureFromPath("ui/uld/ReadyCheck.tex"); + + public bool Visible = false; + public Vector2 Position; + + private SmoothHPHelper _smoothHPHelper = new SmoothHPHelper(); + + private bool _wasHovering = false; + + public IPartyFramesMember? Member; + + public PartyFramesBar(string id, PartyFramesConfigs configs) + { + _configs = configs; + + _nameLabelHud = new LabelHud(_configs.HealthBar.NameLabelConfig); + _healthLabelHud = new LabelHud(_configs.HealthBar.HealthLabelConfig); + _orderLabelHud = new LabelHud(_configs.HealthBar.OrderNumberConfig); + _statusLabelHud = new LabelHud(PlayerStatus.Label); + _raiseLabelHud = new LabelHud(RaiseTracker.Icon.NumericLabel); + _invulnLabelHud = new LabelHud(InvulnTracker.Icon.NumericLabel); + + _manaBarHud = new PrimaryResourceHud(_configs.ManaBar); + _castbarHud = new CastbarHud(_configs.CastBar); + _buffsListHud = new StatusEffectsListHud(_configs.Buffs); + _debuffsListHud = new StatusEffectsListHud(_configs.Debuffs); + + _cooldownListHud = new PartyFramesCooldownListHud(_configs.CooldownList); + } + + public PluginConfigColor GetColor(float scale) + { + if (Member == null || Member.MaxHP <= 0) + { + return _configs.HealthBar.ColorsConfig.OutOfReachBackgroundColor; + } + + bool cleanseCheck = true; + if (CleanseTracker.CleanseJobsOnly) + { + cleanseCheck = Utils.IsOnCleanseJob(); + } + + if (CleanseTracker.Enabled && CleanseTracker.ChangeHealthBarCleanseColor && Member.HasDispellableDebuff && cleanseCheck) + { + return CleanseTracker.HealthBarColor; + } + else if (_configs.HealthBar.ColorsConfig.ColorByHealth.Enabled) + { + if (_configs.HealthBar.ColorsConfig.ColorByHealth.UseJobColorAsMaxHealth) + { + return ColorUtils.GetColorByScale(scale, _configs.HealthBar.ColorsConfig.ColorByHealth.LowHealthColorThreshold / 100f, _configs.HealthBar.ColorsConfig.ColorByHealth.FullHealthColorThreshold / 100f, _configs.HealthBar.ColorsConfig.ColorByHealth.LowHealthColor, _configs.HealthBar.ColorsConfig.ColorByHealth.FullHealthColor, + GlobalColors.Instance.SafeColorForJobId(Member.JobId), _configs.HealthBar.ColorsConfig.ColorByHealth.UseMaxHealthColor, _configs.HealthBar.ColorsConfig.ColorByHealth.BlendMode); + } + else if (_configs.HealthBar.ColorsConfig.ColorByHealth.UseRoleColorAsMaxHealth) + { + return ColorUtils.GetColorByScale(scale, _configs.HealthBar.ColorsConfig.ColorByHealth.LowHealthColorThreshold / 100f, _configs.HealthBar.ColorsConfig.ColorByHealth.FullHealthColorThreshold / 100f, _configs.HealthBar.ColorsConfig.ColorByHealth.LowHealthColor, _configs.HealthBar.ColorsConfig.ColorByHealth.FullHealthColor, + GlobalColors.Instance.SafeRoleColorForJobId(Member.JobId), _configs.HealthBar.ColorsConfig.ColorByHealth.UseMaxHealthColor, _configs.HealthBar.ColorsConfig.ColorByHealth.BlendMode); + } + return ColorUtils.GetColorByScale(scale, _configs.HealthBar.ColorsConfig.ColorByHealth); + } + else if (Member.JobId > 0) + { + return _configs.HealthBar.ColorsConfig.UseRoleColors switch + { + true => GlobalColors.Instance.SafeRoleColorForJobId(Member.JobId), + _ => GlobalColors.Instance.SafeColorForJobId(Member.JobId) + }; + } + + return Member.Character?.ObjectKind switch + { + ObjectKind.BattleNpc => GlobalColors.Instance.NPCFriendlyColor, + _ => _configs.HealthBar.ColorsConfig.OutOfReachBackgroundColor + }; + } + + public void StopPreview() + { + _castbarHud.StopPreview(); + _buffsListHud.StopPreview(); + _debuffsListHud.StopPreview(); + _cooldownListHud.StopPreview(); + _configs.HealthBar.MouseoverAreaConfig.Preview = false; + } + + public void StopMouseover() + { + if (_wasHovering) + { + InputsHelper.Instance.ClearTarget(); + _wasHovering = false; + } + } + + public void Dispose() + { + _cooldownListHud.Dispose(); + } + + public List<(StrataLevel, Action)> GetBarDrawActions(Vector2 origin, PluginConfigColor? borderColor = null) + { + List<(StrataLevel, Action)> drawActions = new List<(StrataLevel, Action)>(); + + if (!Visible || Member is null) + { + StopMouseover(); + return drawActions; + } + + // click + var (areaStart, areaEnd) = _configs.HealthBar.MouseoverAreaConfig.GetArea(Position, _configs.HealthBar.Size); + bool isHovering = ImGui.IsMouseHoveringRect(areaStart, areaEnd); + bool ignoreMouseover = _configs.HealthBar.MouseoverAreaConfig.Enabled && _configs.HealthBar.MouseoverAreaConfig.Ignore; + ICharacter? character = Member.Character; + + if (isHovering) + { + _wasHovering = true; + InputsHelper.Instance.SetTarget(character, ignoreMouseover); + + // left click + if (InputsHelper.Instance.LeftButtonClicked && character != null) + { + Plugin.TargetManager.Target = character; + } + // right click (context menu) + else if (InputsHelper.Instance.RightButtonClicked) + { + OpenContextMenuEvent?.Invoke(this); + } + } + else if (_wasHovering) + { + InputsHelper.Instance.ClearTarget(); + _wasHovering = false; + } + + // bg + PluginConfigColor bgColor = _configs.HealthBar.ColorsConfig.BackgroundColor; + if (Member.RaiseTime != null && RaiseTracker.Enabled && RaiseTracker.ChangeBackgroundColorWhenRaised) + { + bgColor = RaiseTracker.BackgroundColor; + } + else if (Member.InvulnStatus?.InvulnTime != null && InvulnTracker.Enabled && InvulnTracker.ChangeBackgroundColorWhenInvuln) + { + bgColor = Member.InvulnStatus?.InvulnId == 811 ? InvulnTracker.WalkingDeadBackgroundColor : InvulnTracker.BackgroundColor; + } + else if (_configs.HealthBar.ColorsConfig.UseDeathIndicatorBackgroundColor && Member.HP <= 0 && character != null) + { + bgColor = _configs.HealthBar.RangeConfig.Enabled + ? GetDistanceColor(character, _configs.HealthBar.ColorsConfig.DeathIndicatorBackgroundColor) + : _configs.HealthBar.ColorsConfig.DeathIndicatorBackgroundColor; + } + else if (_configs.HealthBar.ColorsConfig.UseJobColorAsBackgroundColor && character is IBattleChara) + { + bgColor = GlobalColors.Instance.SafeColorForJobId(character.ClassJob.RowId); + } + else if (_configs.HealthBar.ColorsConfig.UseRoleColorAsBackgroundColor && character is IBattleChara) + { + bgColor = _configs.HealthBar.RangeConfig.Enabled + ? GetDistanceColor(character, GlobalColors.Instance.SafeRoleColorForJobId(character.ClassJob.RowId)) + : GlobalColors.Instance.SafeRoleColorForJobId(character.ClassJob.RowId); + } + + Rect background = new Rect(Position, _configs.HealthBar.Size, bgColor); + + // hp + uint currentHp = Member.HP; + uint maxHp = Member.MaxHP; + + if (_configs.HealthBar.SmoothHealthConfig.Enabled) + { + currentHp = _smoothHPHelper.GetNextHp((int)currentHp, (int)maxHp, _configs.HealthBar.SmoothHealthConfig.Velocity); + } + + float hpScale = maxHp > 0 ? (float)currentHp / (float)maxHp : 1; + PluginConfigColor? hpColor = _configs.HealthBar.RangeConfig.Enabled && character != null + ? GetDistanceColor(character, GetColor(hpScale)) + : GetColor(hpScale); + + Rect healthFill = BarUtilities.GetFillRect(Position, _configs.HealthBar.Size, _configs.HealthBar.FillDirection, hpColor, currentHp, maxHp); + + // bar + int thickness = borderColor != null ? _configs.HealthBar.ColorsConfig.ActiveBorderThickness : _configs.HealthBar.ColorsConfig.InactiveBorderThickness; + + if (WhosTalkingIcon.ChangeBorders && Member.WhosTalkingState != WhosTalkingState.None) + { + thickness = WhosTalkingIcon.BorderThickness; + } + + borderColor = borderColor ?? GetBorderColor(character); + + BarHud bar = new BarHud( + _configs.HealthBar.ID, + _configs.HealthBar.ColorsConfig.ShowBorder, + borderColor, + thickness, + actor: character, + current: currentHp, + max: maxHp, + shadowConfig: _configs.HealthBar.ShadowConfig, + barTextureName: _configs.HealthBar.BarTextureName, + barTextureDrawMode: _configs.HealthBar.BarTextureDrawMode + ); + + bar.NeedsInputs = true; + bar.SetBackground(background); + bar.AddForegrounds(healthFill); + + // missing health + if (_configs.HealthBar.ColorsConfig.UseMissingHealthBar) + { + Vector2 healthMissingSize = _configs.HealthBar.Size - BarUtilities.GetFillDirectionOffset(healthFill.Size, _configs.HealthBar.FillDirection); + Vector2 healthMissingPos = _configs.HealthBar.FillDirection.IsInverted() ? Position : Position + BarUtilities.GetFillDirectionOffset(healthFill.Size, _configs.HealthBar.FillDirection); + + PluginConfigColor? missingHealthColor = _configs.HealthBar.ColorsConfig.UseJobColorAsMissingHealthColor && character is IBattleChara + ? GlobalColors.Instance.SafeColorForJobId(character!.ClassJob.RowId) + : _configs.HealthBar.ColorsConfig.UseRoleColorAsMissingHealthColor && character is IBattleChara + ? GlobalColors.Instance.SafeRoleColorForJobId(character!.ClassJob.RowId) + : _configs.HealthBar.ColorsConfig.HealthMissingColor; + + if (_configs.HealthBar.ColorsConfig.UseDeathIndicatorBackgroundColor && Member.HP <= 0 && character != null) + { + missingHealthColor = _configs.HealthBar.ColorsConfig.DeathIndicatorBackgroundColor; + } + + if (_configs.Trackers.Invuln.ChangeBackgroundColorWhenInvuln && character is IBattleChara battleChara) + { + IStatus? tankInvuln = Utils.GetTankInvulnerabilityID(battleChara); + if (tankInvuln is not null) + { + missingHealthColor = _configs.Trackers.Invuln.BackgroundColor; + } + } + + if (_configs.HealthBar.RangeConfig.Enabled) + { + missingHealthColor = GetDistanceColor(character, missingHealthColor); + } + + bar.AddForegrounds(new Rect(healthMissingPos, healthMissingSize, missingHealthColor)); + } + + // shield + if (_configs.HealthBar.ShieldConfig.Enabled) + { + if (Member.Shield > 0f) + { + bar.AddForegrounds( + BarUtilities.GetShieldForeground( + _configs.HealthBar.ShieldConfig, + Position, + _configs.HealthBar.Size, + healthFill.Size, + _configs.HealthBar.FillDirection, + Member.Shield, + currentHp, + maxHp + ) + ); + } + } + + // highlight + bool isSoftTarget = character != null && character == Plugin.TargetManager.SoftTarget; + if (_configs.HealthBar.ColorsConfig.ShowHighlight && (isHovering || isSoftTarget)) + { + Rect highlight = new Rect(Position, _configs.HealthBar.Size, _configs.HealthBar.ColorsConfig.HighlightColor); + bar.AddForegrounds(highlight); + } + + drawActions = bar.GetDrawActions(Vector2.Zero, _configs.HealthBar.StrataLevel); + + // mouseover area + BarHud? mouseoverAreaBar = _configs.HealthBar.MouseoverAreaConfig.GetBar( + Position, + _configs.HealthBar.Size, + _configs.HealthBar.ID + "_mouseoverArea" + ); + + if (mouseoverAreaBar != null) + { + drawActions.AddRange(mouseoverAreaBar.GetDrawActions(Vector2.Zero, StrataLevel.HIGHEST)); + } + + return drawActions; + } + + private PluginConfigColor GetBorderColor(ICharacter? character) + { + IGameObject? target = Plugin.TargetManager.Target ?? Plugin.TargetManager.SoftTarget; + + return character != null && character == target ? _configs.HealthBar.ColorsConfig.TargetBordercolor : _configs.HealthBar.ColorsConfig.BorderColor; + } + + private PluginConfigColor GetDistanceColor(ICharacter? character, PluginConfigColor color) + { + byte distance = character != null ? character.YalmDistanceX : byte.MaxValue; + float currentAlpha = color.Vector.W * 100f; + float alpha = _configs.HealthBar.RangeConfig.AlphaForDistance(distance, currentAlpha) / 100f; + + return color.WithAlpha(alpha); + } + + // need to separate elements that have their own window so clipping doesn't get messy + public List<(StrataLevel, Action)> GetElementsDrawActions(Vector2 origin) + { + List<(StrataLevel, Action)> drawActions = new List<(StrataLevel, Action)>(); + + IPlayerCharacter? player = Plugin.ObjectTable.LocalPlayer; + if (!Visible || Member is null || player == null) + { + StopMouseover(); + return drawActions; + } + + ICharacter? character = Member.Character; + + // who's talking + bool drawingWhosTalking = false; + if (WhosTalkingIcon.Enabled && WhosTalkingIcon.Icon.Enabled && WhosTalkingIcon.EnabledForState(Member.WhosTalkingState)) + { + IDalamudTextureWrap? texture = WhosTalkingHelper.Instance.GetTextureForState(Member.WhosTalkingState); + + if (texture != null) + { + Vector2 parentPos = Utils.GetAnchoredPosition(Position, -_configs.HealthBar.Size, WhosTalkingIcon.Icon.FrameAnchor); + Vector2 iconPos = Utils.GetAnchoredPosition(parentPos + WhosTalkingIcon.Icon.Position, WhosTalkingIcon.Icon.Size, WhosTalkingIcon.Icon.Anchor); + + drawActions.Add((WhosTalkingIcon.Icon.StrataLevel, () => + { + DrawHelper.DrawInWindow(WhosTalkingIcon.Icon.ID, iconPos, WhosTalkingIcon.Icon.Size, false, (drawList) => + { + ImGui.SetCursorPos(iconPos); + ImGui.Image(texture.Handle, WhosTalkingIcon.Icon.Size); + }); + } + )); + + drawingWhosTalking = true; + } + } + + // role/job icon + if (RoleIcon.Enabled && (!drawingWhosTalking || !WhosTalkingIcon.ReplaceRoleJobIcon)) + { + uint iconId = 0; + + // chocobo icon + if (character is IBattleNpc battleNpc && battleNpc.BattleNpcKind == BattleNpcSubKind.Chocobo) + { + iconId = JobsHelper.RoleIconIDForBattleCompanion + (uint)RoleIcon.Style * 100; + } + // role/job icon + else if (Member.JobId > 0) + { + iconId = RoleIcon.UseRoleIcons ? + JobsHelper.RoleIconIDForJob(Member.JobId, RoleIcon.UseSpecificDPSRoleIcons) : + JobsHelper.IconIDForJob(Member.JobId, (uint)RoleIcon.Style); + } + + if (iconId > 0) + { + Vector2 parentPos = Utils.GetAnchoredPosition(Position, -_configs.HealthBar.Size, RoleIcon.FrameAnchor); + Vector2 iconPos = Utils.GetAnchoredPosition(parentPos + RoleIcon.Position, RoleIcon.Size, RoleIcon.Anchor); + + drawActions.Add((RoleIcon.StrataLevel, () => + { + DrawHelper.DrawInWindow(RoleIcon.ID, iconPos, RoleIcon.Size, false, (drawList) => + { + DrawHelper.DrawIcon(iconId, iconPos, RoleIcon.Size, false, drawList); + }); + } + )); + } + } + + // sign icon + if (SignIcon.Enabled) + { + uint? iconId = SignIcon.IconID(character); + if (iconId.HasValue) + { + Vector2 parentPos = Utils.GetAnchoredPosition(Position, -_configs.HealthBar.Size, SignIcon.FrameAnchor); + Vector2 iconPos = Utils.GetAnchoredPosition(parentPos + SignIcon.Position, SignIcon.Size, SignIcon.Anchor); + + drawActions.Add((SignIcon.StrataLevel, () => + { + DrawHelper.DrawInWindow(SignIcon.ID, iconPos, SignIcon.Size, false, (drawList) => + { + DrawHelper.DrawIcon(iconId.Value, iconPos, SignIcon.Size, false, drawList); + }); + } + )); + } + } + + // leader icon + if (LeaderIcon.Enabled && Member.IsPartyLeader) + { + Vector2 parentPos = Utils.GetAnchoredPosition(Position, -_configs.HealthBar.Size, LeaderIcon.FrameAnchor); + Vector2 iconPos = Utils.GetAnchoredPosition(parentPos + LeaderIcon.Position, LeaderIcon.Size, LeaderIcon.Anchor); + + drawActions.Add((LeaderIcon.StrataLevel, () => + { + DrawHelper.DrawInWindow(LeaderIcon.ID, iconPos, LeaderIcon.Size, false, (drawList) => + { + DrawHelper.DrawIcon(61521, iconPos, LeaderIcon.Size, false, drawList); + }); + } + )); + } + + // player status icon + if (PlayerStatus.Enabled && PlayerStatus.Icon.Enabled) + { + uint? iconId = IconIdForStatus(Member.Status); + if (iconId.HasValue) + { + Vector2 parentPos = Utils.GetAnchoredPosition(Position, -_configs.HealthBar.Size, PlayerStatus.Icon.FrameAnchor); + Vector2 iconPos = Utils.GetAnchoredPosition(parentPos + PlayerStatus.Icon.Position, PlayerStatus.Icon.Size, PlayerStatus.Icon.Anchor); + + drawActions.Add((PlayerStatus.Icon.StrataLevel, () => + { + DrawHelper.DrawInWindow(PlayerStatus.Icon.ID, iconPos, PlayerStatus.Icon.Size, false, (drawList) => + { + DrawHelper.DrawIcon(iconId.Value, iconPos, PlayerStatus.Icon.Size, false, drawList); + }); + } + )); + } + } + + // ready check status icon + if (Member.ReadyCheckStatus != ReadyCheckStatus.None && ReadyCheckIcon.Enabled && ReadyCheckIcon.Icon.Enabled && _readyCheckTexture != null) + { + Vector2 parentPos = Utils.GetAnchoredPosition(Position, -_configs.HealthBar.Size, ReadyCheckIcon.Icon.FrameAnchor); + Vector2 iconPos = Utils.GetAnchoredPosition(parentPos + ReadyCheckIcon.Icon.Position, ReadyCheckIcon.Icon.Size, ReadyCheckIcon.Icon.Anchor); + + drawActions.Add((ReadyCheckIcon.Icon.StrataLevel, () => + { + DrawHelper.DrawInWindow(ReadyCheckIcon.Icon.ID, iconPos, ReadyCheckIcon.Icon.Size, false, (drawList) => + { + Vector2 uv0 = new Vector2(0.5f * (int)Member.ReadyCheckStatus, 0f); + Vector2 uv1 = new Vector2(0.5f + 0.5f * (int)Member.ReadyCheckStatus, 1f); + drawList.AddImage(_readyCheckTexture.Handle, iconPos, iconPos + ReadyCheckIcon.Icon.Size, uv0, uv1); + }); + } + )); + } + + // raise icon + bool showingRaise = ShowingRaise(); + if (showingRaise && RaiseTracker.Icon.Enabled) + { + Vector2 parentPos = Utils.GetAnchoredPosition(Position, -_configs.HealthBar.Size, RaiseTracker.Icon.FrameAnchor); + Vector2 iconPos = Utils.GetAnchoredPosition(parentPos + RaiseTracker.Icon.Position, RaiseTracker.Icon.Size, RaiseTracker.Icon.Anchor); + + drawActions.Add((RaiseTracker.Icon.StrataLevel, () => + { + DrawHelper.DrawInWindow(RaiseTracker.Icon.ID, iconPos, RaiseTracker.Icon.Size, false, (drawList) => + { + DrawHelper.DrawIcon(411, iconPos, RaiseTracker.Icon.Size, true, drawList); + }); + } + )); + } + + // invuln icon + bool showingInvuln = ShowingInvuln(); + if (showingInvuln && InvulnTracker.Icon.Enabled) + { + Vector2 parentPos = Utils.GetAnchoredPosition(Position, -_configs.HealthBar.Size, InvulnTracker.Icon.FrameAnchor); + Vector2 iconPos = Utils.GetAnchoredPosition(parentPos + InvulnTracker.Icon.Position, InvulnTracker.Icon.Size, InvulnTracker.Icon.Anchor); + + drawActions.Add((InvulnTracker.Icon.StrataLevel, () => + { + DrawHelper.DrawInWindow(InvulnTracker.Icon.ID, iconPos, InvulnTracker.Icon.Size, false, (drawList) => + { + DrawHelper.DrawIcon(Member.InvulnStatus!.InvulnIcon, iconPos, InvulnTracker.Icon.Size, true, drawList); + }); + } + )); + } + + // mana + if (ShowMana()) + { + Vector2 parentPos = Utils.GetAnchoredPosition(Position, -_configs.HealthBar.Size, _configs.ManaBar.HealthBarAnchor); + drawActions.Add((_configs.ManaBar.StrataLevel, () => + { + _manaBarHud.Actor = character; + _manaBarHud.PartyMember = Member; + _manaBarHud.PrepareForDraw(parentPos); + _manaBarHud.Draw(parentPos); + } + )); + } + + // buffs / debuffs + Vector2 buffsPos = Utils.GetAnchoredPosition(Position, -_configs.HealthBar.Size, _configs.Buffs.HealthBarAnchor); + drawActions.Add((_configs.Buffs.StrataLevel, () => + { + _buffsListHud.Actor = character; + _buffsListHud.PrepareForDraw(buffsPos); + _buffsListHud.Draw(buffsPos); + } + )); + + Vector2 debuffsPos = Utils.GetAnchoredPosition(Position, -_configs.HealthBar.Size, _configs.Debuffs.HealthBarAnchor); + drawActions.Add((_configs.Debuffs.StrataLevel, () => + { + _debuffsListHud.Actor = character; + _debuffsListHud.PrepareForDraw(debuffsPos); + _debuffsListHud.Draw(debuffsPos); + } + )); + + // cooldown list + Vector2 cooldownListPos = Utils.GetAnchoredPosition(Position, -_configs.HealthBar.Size, _configs.CooldownList.HealthBarAnchor); + drawActions.Add((_configs.CooldownList.StrataLevel, () => + { + _cooldownListHud.Actor = character; + _cooldownListHud.PrepareForDraw(cooldownListPos); + _cooldownListHud.Draw(cooldownListPos); + } + )); + + // castbar + Vector2 castbarPos = Utils.GetAnchoredPosition(Position, -_configs.HealthBar.Size, _configs.CastBar.HealthBarAnchor); + drawActions.Add((_configs.CastBar.StrataLevel, () => + { + _castbarHud.Actor = character; + _castbarHud.PrepareForDraw(castbarPos); + _castbarHud.Draw(castbarPos); + } + )); + + // name + bool drawName = ShouldDrawName(character, showingRaise, showingInvuln); + if (drawName) + { + drawActions.Add((_configs.HealthBar.NameLabelConfig.StrataLevel, () => + { + bool? playerName = null; + if (character == null || character.ObjectKind == ObjectKind.Player) + { + playerName = true; + } + + _nameLabelHud.Draw(Position, _configs.HealthBar.Size, character, Member.Name, isPlayerName: playerName); + } + )); + } + + // health label + if (Member.MaxHP > 0) + { + drawActions.Add((_configs.HealthBar.HealthLabelConfig.StrataLevel, () => + { + _healthLabelHud.Draw(Position, _configs.HealthBar.Size, character, null, Member.HP, Member.MaxHP); + } + )); + } + + // order + if (character == null || character?.ObjectKind != ObjectKind.BattleNpc) + { + string str = char.ConvertFromUtf32(0xE090 + Member.Order).ToString(); + + drawActions.Add((_configs.HealthBar.OrderNumberConfig.StrataLevel, () => + { + _configs.HealthBar.OrderNumberConfig.SetText(str); + _orderLabelHud.Draw(Position, _configs.HealthBar.Size, character); + } + )); + } + + // status + string? statusString = StringForStatus(Member.Status); + if (PlayerStatus.Enabled && PlayerStatus.Label.Enabled && statusString != null) + { + drawActions.Add((PlayerStatus.Label.StrataLevel, () => + { + PlayerStatus.Label.SetText(statusString); + _statusLabelHud.Draw(Position, _configs.HealthBar.Size); + } + )); + } + + // raise label + if (showingRaise) + { + float duration = Math.Abs(Member.RaiseTime!.Value); + + drawActions.Add((RaiseTracker.Icon.NumericLabel.StrataLevel, () => + { + RaiseTracker.Icon.NumericLabel.SetValue(duration); + _raiseLabelHud.Draw(Position, _configs.HealthBar.Size); + } + )); + } + + // invuln label + if (showingInvuln) + { + float duration = Math.Abs(Member.InvulnStatus!.InvulnTime); + + drawActions.Add((InvulnTracker.Icon.NumericLabel.StrataLevel, () => + { + InvulnTracker.Icon.NumericLabel.SetValue(duration); + _invulnLabelHud.Draw(Position, _configs.HealthBar.Size); + } + )); + } + + return drawActions; + } + + private bool ShouldDrawName(ICharacter? character, bool showingRaise, bool showingInvuln) + { + if (showingRaise && RaiseTracker.HideNameWhenRaised) + { + return false; + } + + if (showingInvuln && InvulnTracker.HideNameWhenInvuln) + { + return false; + } + + if (Member != null && PlayerStatus.Enabled && PlayerStatus.HideName && Member.Status != PartyMemberStatus.None) + { + return false; + } + + if (Member != null && ReadyCheckIcon.Enabled && ReadyCheckIcon.HideName && Member.ReadyCheckStatus != ReadyCheckStatus.None) + { + return false; + } + + if (Utils.IsActorCasting(character) && _configs.CastBar.Enabled && _configs.CastBar.HideNameWhenCasting) + { + return false; + } + + return true; + } + + private bool ShowingRaise() => + Member != null && Member.RaiseTime.HasValue && RaiseTracker.Enabled && + (Member.RaiseTime.Value > 0 || RaiseTracker.KeepIconAfterCastFinishes); + + private bool ShowingInvuln() => Member != null && Member.InvulnStatus != null && InvulnTracker.Enabled && Member.InvulnStatus.InvulnTime > 0; + + private bool ShowMana() + { + if (Member == null) + { + return false; + } + + var isHealer = JobsHelper.IsJobHealer(Member.JobId); + + return _configs.ManaBar.Enabled && Member.MaxHP > 0 && _configs.ManaBar.ManaBarDisplayMode switch + { + PartyFramesManaBarDisplayMode.Always => true, + PartyFramesManaBarDisplayMode.HealersOnly => isHealer, + PartyFramesManaBarDisplayMode.HealersAndRaiseJobs => isHealer || JobsHelper.IsJobWithRaise(Member.JobId, Member.Level), + _ => true + }; + } + + private static uint? IconIdForStatus(PartyMemberStatus status) + { + return status switch + { + PartyMemberStatus.ViewingCutscene => 61508, + PartyMemberStatus.Offline => 61504, + PartyMemberStatus.Dead => 61502, + _ => null + }; + } + + private static string? StringForStatus(PartyMemberStatus status) + { + return status switch + { + PartyMemberStatus.ViewingCutscene => "[Viewing Cutscene]", + PartyMemberStatus.Offline => "[Offline]", + PartyMemberStatus.Dead => "[Dead]", + _ => null + }; + } + + #region convenience + private PartyFramesRoleIconConfig RoleIcon => _configs.Icons.Role; + private SignIconConfig SignIcon => _configs.Icons.Sign; + private PartyFramesLeaderIconConfig LeaderIcon => _configs.Icons.Leader; + private PartyFramesPlayerStatusConfig PlayerStatus => _configs.Icons.PlayerStatus; + private PartyFramesReadyCheckStatusConfig ReadyCheckIcon => _configs.Icons.ReadyCheckStatus; + private PartyFramesWhosTalkingConfig WhosTalkingIcon => _configs.Icons.WhosTalking; + private PartyFramesRaiseTrackerConfig RaiseTracker => _configs.Trackers.Raise; + private PartyFramesInvulnTrackerConfig InvulnTracker => _configs.Trackers.Invuln; + private PartyFramesCleanseTrackerConfig CleanseTracker => _configs.Trackers.Cleanse; + #endregion + } +} diff --git a/Interface/Party/PartyFramesCleanseTracker.cs b/Interface/Party/PartyFramesCleanseTracker.cs new file mode 100644 index 0000000..d640137 --- /dev/null +++ b/Interface/Party/PartyFramesCleanseTracker.cs @@ -0,0 +1,80 @@ +using Dalamud.Game.ClientState.Objects.Types; +using HSUI.Config; +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Game.ClientState.Objects.SubKinds; +using HSUI.Helpers; +using Dalamud.Game.ClientState.Statuses; + +namespace HSUI.Interface.Party +{ + public class PartyFramesCleanseTracker : IDisposable + { + private PartyFramesCleanseTrackerConfig _config = null!; + + public PartyFramesCleanseTracker() + { + ConfigurationManager.Instance.ResetEvent += OnConfigReset; + OnConfigReset(ConfigurationManager.Instance); + } + + ~PartyFramesCleanseTracker() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + + ConfigurationManager.Instance.ResetEvent -= OnConfigReset; + } + + public void OnConfigReset(ConfigurationManager sender) + { + _config = ConfigurationManager.Instance.GetConfigObject().Cleanse; + } + + public void Update(List partyMembers) + { + if (!_config.Enabled) + { + return; + } + + foreach (var member in partyMembers) + { + member.HasDispellableDebuff = false; + + if (member.Character is not IBattleChara battleChara) + { + continue; + } + + // check for disspellable debuff + IEnumerable statusList = Utils.StatusListForBattleChara(battleChara); + foreach (IStatus status in statusList) + { + if (!status.GameData.Value.CanDispel) + { + continue; + } + + // apply raise data based on buff + member.HasDispellableDebuff = true; + break; + } + } + } + } +} diff --git a/Interface/Party/PartyFramesConfig.cs b/Interface/Party/PartyFramesConfig.cs new file mode 100644 index 0000000..2ff64cb --- /dev/null +++ b/Interface/Party/PartyFramesConfig.cs @@ -0,0 +1,852 @@ +using HSUI.Config; +using HSUI.Config.Attributes; +using HSUI.Enums; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using HSUI.Interface.GeneralElements; +using HSUI.Interface.PartyCooldowns; +using HSUI.Interface.StatusEffects; +using Dalamud.Bindings.ImGui; +using System; +using System.Numerics; + +namespace HSUI.Interface.Party +{ + [Exportable(false)] + [Section("Party Frames", true)] + [SubSection("General", 0)] + public class PartyFramesConfig : MovablePluginConfigObject + { + public new static PartyFramesConfig DefaultConfig() + { + var config = new PartyFramesConfig(); + config.Position = new Vector2(-ImGui.GetMainViewport().Size.X / 3 - 180, -120); + + return config; + } + + [Checkbox("Preview", isMonitored = true)] + [Order(4)] + public bool Preview = false; + + [DragInt("Rows", spacing = true, isMonitored = true, min = 1, max = 8, velocity = 0.2f)] + [Order(10)] + public int Rows = 4; + + [DragInt("Columns", isMonitored = true, min = 1, max = 8, velocity = 0.2f)] + [Order(11)] + public int Columns = 2; + + [Anchor("Bars Anchor", isMonitored = true, spacing = true)] + [Order(15)] + public DrawAnchor BarsAnchor = DrawAnchor.TopLeft; + + [Checkbox("Fill Rows First", isMonitored = true)] + [Order(20)] + public bool FillRowsFirst = true; + + [Checkbox("Show When Solo", spacing = true)] + [Order(50)] + public bool ShowWhenSolo = false; + + [Checkbox("Show Chocobo", isMonitored = true)] + [Order(55)] + public bool ShowChocobo = true; + + [NestedConfig("Party Title Label", 60)] + public PartyFramesTitleLabel ShowPartyTitleConfig = new PartyFramesTitleLabel(Vector2.Zero, "", DrawAnchor.Left, DrawAnchor.Left); + + [NestedConfig("Visibility", 200)] + public VisibilityConfig VisibilityConfig = new VisibilityConfig(); + } + + [Exportable(false)] + [DisableParentSettings("FrameAnchor", "UseJobColor", "UseRoleColor")] + public class PartyFramesTitleLabel : LabelConfig + { + public PartyFramesTitleLabel(Vector2 position, string text, DrawAnchor frameAnchor, DrawAnchor textAnchor) : base(position, text, frameAnchor, textAnchor) + { + } + } + + [Exportable(false)] + [Disableable(false)] + [DisableParentSettings("Position", "Anchor", "BackgroundColor", "FillColor", "HideWhenInactive", "DrawBorder", "BorderColor", "BorderThickness")] + [Section("Party Frames", true)] + [SubSection("Health Bar", 0)] + public class PartyFramesHealthBarsConfig : BarConfig + { + public new static PartyFramesHealthBarsConfig DefaultConfig() + { + var config = new PartyFramesHealthBarsConfig(Vector2.Zero, new(180, 80), PluginConfigColor.Empty); + config.MouseoverAreaConfig.Enabled = false; + + return config; + } + + [DragInt2("Padding", isMonitored = true, min = 0)] + [Order(31)] + public Vector2 Padding = new Vector2(0, 0); + + [NestedConfig("Name Label", 44)] + public EditableLabelConfig NameLabelConfig = new EditableLabelConfig(Vector2.Zero, "[name:initials].", DrawAnchor.Center, DrawAnchor.Center); + + [NestedConfig("Health Label", 45)] + public EditableLabelConfig HealthLabelConfig = new EditableLabelConfig(Vector2.Zero, "[health:current-short]", DrawAnchor.Right, DrawAnchor.Right); + + [NestedConfig("Order Label", 50)] + public DefaultFontLabelConfig OrderNumberConfig = new DefaultFontLabelConfig(new Vector2(2, 4), "", DrawAnchor.TopLeft, DrawAnchor.TopLeft); + + [NestedConfig("Colors", 55)] + public PartyFramesColorsConfig ColorsConfig = new PartyFramesColorsConfig(); + + [NestedConfig("Shield", 60)] + public ShieldConfig ShieldConfig = new ShieldConfig(); + + [NestedConfig("Change Alpha Based on Range", 65)] + public PartyFramesRangeConfig RangeConfig = new PartyFramesRangeConfig(); + + [NestedConfig("Use Smooth Transitions", 70)] + public SmoothHealthConfig SmoothHealthConfig = new SmoothHealthConfig(); + + [NestedConfig("Custom Mouseover Area", 75)] + public MouseoverAreaConfig MouseoverAreaConfig = new MouseoverAreaConfig(); + + public PartyFramesHealthBarsConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor, BarDirection fillDirection = BarDirection.Right) + : base(position, size, fillColor, fillDirection) + { + } + } + + [Disableable(false)] + [Exportable(false)] + public class PartyFramesColorsConfig : PluginConfigObject + { + [Checkbox("Show Border")] + [Order(4)] + public bool ShowBorder = true; + + [ColorEdit4("Border Color")] + [Order(5, collapseWith = nameof(ShowBorder))] + public PluginConfigColor BorderColor = new PluginConfigColor(new Vector4(0f / 255f, 0f / 255f, 0f / 255f, 100f / 100f)); + + [ColorEdit4("Target Border Color")] + [Order(6, collapseWith = nameof(ShowBorder))] + public PluginConfigColor TargetBordercolor = new PluginConfigColor(new Vector4(255f / 255f, 255f / 255f, 255f / 255f, 100f / 100f)); + + [DragInt("Inactive Border Thickness", min = 1, max = 10, help = "This is the border thickness that will be used when the border is in the default state (aka not targetted, not showing enmity, etc).")] + [Order(6, collapseWith = nameof(ShowBorder))] + public int InactiveBorderThickness = 1; + + [DragInt("Active Border Thickness", min = 1, max = 10, help = "This is the border thickness that will be used when the border active (aka targetted, showing enmity, etc).")] + [Order(7, collapseWith = nameof(ShowBorder))] + public int ActiveBorderThickness = 1; + + [ColorEdit4("Background Color", spacing = true)] + [Order(15)] + public PluginConfigColor BackgroundColor = new PluginConfigColor(new Vector4(0f / 255f, 0f / 255f, 0f / 255f, 70f / 100f)); + + [ColorEdit4("Out of Reach Background Color", help = "This background color will be used when the player's data couldn't be retreived (i.e. player is disconnected)")] + [Order(15)] + public PluginConfigColor OutOfReachBackgroundColor = new PluginConfigColor(new Vector4(50f / 255f, 50f / 255f, 50f / 255f, 70f / 100f)); + + [Checkbox("Use Death Indicator Background Color", isMonitored = true, spacing = true)] + [Order(18)] + public bool UseDeathIndicatorBackgroundColor = false; + + [ColorEdit4("Death Indicator Background Color")] + [Order(19, collapseWith = nameof(UseDeathIndicatorBackgroundColor))] + public PluginConfigColor DeathIndicatorBackgroundColor = new PluginConfigColor(new Vector4(204f / 255f, 3f / 255f, 3f / 255f, 80f / 100f)); + + [Checkbox("Use Role Colors", isMonitored = true, spacing = true)] + [Order(20)] + public bool UseRoleColors = false; + + [NestedConfig("Color Based On Health Value", 30, collapsingHeader = false)] + public ColorByHealthValueConfig ColorByHealth = new ColorByHealthValueConfig(); + + [Checkbox("Highlight When Hovering With Cursor Or Soft Targeting", spacing = true)] + [Order(40)] + public bool ShowHighlight = true; + + [ColorEdit4("Highlight Color")] + [Order(45, collapseWith = nameof(ShowHighlight))] + public PluginConfigColor HighlightColor = new PluginConfigColor(new Vector4(255f / 255f, 255f / 255f, 255f / 255f, 5f / 100f)); + + + [Checkbox("Missing Health Color", spacing = true)] + [Order(46)] + public bool UseMissingHealthBar = false; + + [Checkbox("Job Color As Missing Health Color")] + [Order(47, collapseWith = nameof(UseMissingHealthBar))] + public bool UseJobColorAsMissingHealthColor = false; + + [Checkbox("Role Color As Missing Health Color")] + [Order(48, collapseWith = nameof(UseMissingHealthBar))] + public bool UseRoleColorAsMissingHealthColor = false; + + [ColorEdit4("Color" + "##MissingHealth")] + [Order(49, collapseWith = nameof(UseMissingHealthBar))] + public PluginConfigColor HealthMissingColor = new(new Vector4(255f / 255f, 0f / 255f, 0f / 255f, 100f / 100f)); + + [Checkbox("Job Color As Background Color")] + [Order(50)] + public bool UseJobColorAsBackgroundColor = false; + + [Checkbox("Role Color As Background Color")] + [Order(51)] + public bool UseRoleColorAsBackgroundColor = false; + + [Checkbox("Show Enmity Border Colors", spacing = true)] + [Order(54)] + public bool ShowEnmityBorderColors = true; + + [ColorEdit4("Enmity Leader Color")] + [Order(55, collapseWith = nameof(ShowEnmityBorderColors))] + public PluginConfigColor EnmityLeaderBordercolor = new PluginConfigColor(new Vector4(255f / 255f, 40f / 255f, 40f / 255f, 100f / 100f)); + + [Checkbox("Show Second Enmity")] + [Order(60, collapseWith = nameof(ShowEnmityBorderColors))] + public bool ShowSecondEnmity = true; + + [Checkbox("Hide Second Enmity in Light Parties")] + [Order(65, collapseWith = nameof(ShowSecondEnmity))] + public bool HideSecondEnmityInLightParties = true; + + [ColorEdit4("Enmity Second Color")] + [Order(70, collapseWith = nameof(ShowSecondEnmity))] + public PluginConfigColor EnmitySecondBordercolor = new PluginConfigColor(new Vector4(255f / 255f, 175f / 255f, 40f / 255f, 100f / 100f)); + } + + [Exportable(false)] + public class PartyFramesRangeConfig : PluginConfigObject + { + [DragInt("Range (yalms)", min = 1, max = 500)] + [Order(5)] + public int Range = 30; + + [DragFloat("Alpha", min = 1, max = 100)] + [Order(10)] + public float Alpha = 25; + + [Checkbox("Use Additional Range Check")] + [Order(15)] + public bool UseAdditionalRangeCheck = false; + + [DragInt("Additional Range (yalms)", min = 1, max = 500)] + [Order(20, collapseWith = nameof(UseAdditionalRangeCheck))] + public int AdditionalRange = 15; + + [DragFloat("Additional Alpha", min = 1, max = 100)] + [Order(25, collapseWith = nameof(UseAdditionalRangeCheck))] + public float AdditionalAlpha = 60; + + public float AlphaForDistance(int distance, float alpha = 100f) + { + if (!Enabled) + { + return 100f; + } + + if (!UseAdditionalRangeCheck) + { + return distance > Range ? Alpha : alpha; + } + + if (Range > AdditionalRange) + { + return distance > Range ? Alpha : (distance > AdditionalRange ? AdditionalAlpha : alpha); + } + + return distance > AdditionalRange ? AdditionalAlpha : (distance > Range ? Alpha : alpha); + } + } + + public class PartyFramesManaBarConfigConverter : PluginConfigObjectConverter + { + public PartyFramesManaBarConfigConverter() + { + NewTypeFieldConverter converter; + converter = new NewTypeFieldConverter( + "PartyFramesManaBarDisplayMode", + PartyFramesManaBarDisplayMode.HealersOnly, + (oldValue) => + { + return oldValue ? PartyFramesManaBarDisplayMode.HealersOnly : PartyFramesManaBarDisplayMode.Always; + }); + + FieldConvertersMap.Add("ShowOnlyForHealers", converter); + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(PartyFramesManaBarConfig); + } + } + + public enum PartyFramesManaBarDisplayMode + { + HealersAndRaiseJobs, + HealersOnly, + Always, + } + + [DisableParentSettings("HideWhenInactive", "Label")] + [Exportable(false)] + [Section("Party Frames", true)] + [SubSection("Mana Bar", 0)] + public class PartyFramesManaBarConfig : PrimaryResourceConfig + { + public new static PartyFramesManaBarConfig DefaultConfig() + { + var config = new PartyFramesManaBarConfig(Vector2.Zero, new(180, 6)); + config.HealthBarAnchor = DrawAnchor.Bottom; + config.Anchor = DrawAnchor.Bottom; + config.ValueLabel.Enabled = false; + return config; + } + + [Anchor("Health Bar Anchor")] + [Order(14)] + public DrawAnchor HealthBarAnchor = DrawAnchor.BottomLeft; + + [RadioSelector("Show For All Jobs With Raise", "Show Only For Healers", "Show For All Jobs")] + [Order(42)] + public PartyFramesManaBarDisplayMode ManaBarDisplayMode = PartyFramesManaBarDisplayMode.HealersOnly; + + public PartyFramesManaBarConfig(Vector2 position, Vector2 size) + : base(position, size) + { + } + } + + [Exportable(false)] + [Section("Party Frames", true)] + [SubSection("Castbar", 0)] + public class PartyFramesCastbarConfig : CastbarConfig + { + public new static PartyFramesCastbarConfig DefaultConfig() + { + var size = new Vector2(182, 10); + var pos = new Vector2(-1, 0); + + var castNameConfig = new LabelConfig(new Vector2(5, 0), "", DrawAnchor.Left, DrawAnchor.Left); + var castTimeConfig = new NumericLabelConfig(new Vector2(-5, 0), "", DrawAnchor.Right, DrawAnchor.Right); + castTimeConfig.Enabled = false; + castTimeConfig.NumberFormat = 1; + + var config = new PartyFramesCastbarConfig(pos, size, castNameConfig, castTimeConfig); + config.HealthBarAnchor = DrawAnchor.BottomLeft; + config.Anchor = DrawAnchor.TopLeft; + config.ShowIcon = false; + config.Enabled = false; + + return config; + } + + [Checkbox("Hide Name When Casting")] + [Order(6)] + public bool HideNameWhenCasting = false; + + [Anchor("Health Bar Anchor")] + [Order(16)] + public DrawAnchor HealthBarAnchor = DrawAnchor.BottomLeft; + + public PartyFramesCastbarConfig(Vector2 position, Vector2 size, LabelConfig castNameConfig, NumericLabelConfig castTimeConfig) + : base(position, size, castNameConfig, castTimeConfig) + { + + } + } + + [Disableable(false)] + [Exportable(false)] + [Section("Party Frames", true)] + [SubSection("Icons", 0)] + public class PartyFramesIconsConfig : PluginConfigObject + { + public new static PartyFramesIconsConfig DefaultConfig() { return new PartyFramesIconsConfig(); } + + [NestedConfig("Role / Job", 10, separator = false)] + public PartyFramesRoleIconConfig Role = new PartyFramesRoleIconConfig( + new Vector2(20, 0), + new Vector2(20, 20), + DrawAnchor.TopLeft, + DrawAnchor.TopLeft + ); + + [NestedConfig("Sign", 11)] + public SignIconConfig Sign = new SignIconConfig( + new Vector2(0, -10), + new Vector2(30, 30), + DrawAnchor.Top, + DrawAnchor.Top + ); + + [NestedConfig("Leader", 12)] + public PartyFramesLeaderIconConfig Leader = new PartyFramesLeaderIconConfig( + new Vector2(-12, -12), + new Vector2(24, 24), + DrawAnchor.TopLeft, + DrawAnchor.TopLeft + ); + + [NestedConfig("Player Status", 13)] + public PartyFramesPlayerStatusConfig PlayerStatus = new PartyFramesPlayerStatusConfig(); + + [NestedConfig("Ready Check Status", 14)] + public PartyFramesReadyCheckStatusConfig ReadyCheckStatus = new PartyFramesReadyCheckStatusConfig(); + + [NestedConfig("Who's Talking", 15)] + public PartyFramesWhosTalkingConfig WhosTalking = new PartyFramesWhosTalkingConfig(); + } + + [Exportable(false)] + public class PartyFramesRoleIconConfig : RoleJobIconConfig + { + public PartyFramesRoleIconConfig() : base() { } + + public PartyFramesRoleIconConfig(Vector2 position, Vector2 size, DrawAnchor anchor, DrawAnchor frameAnchor) + : base(position, size, anchor, frameAnchor) + { + } + } + + [Exportable(false)] + public class PartyFramesLeaderIconConfig : IconConfig + { + public PartyFramesLeaderIconConfig() : base() { } + + public PartyFramesLeaderIconConfig(Vector2 position, Vector2 size, DrawAnchor anchor, DrawAnchor frameAnchor) + : base(position, size, anchor, frameAnchor) + { + } + } + + [Exportable(false)] + public class PartyFramesPlayerStatusConfig : PluginConfigObject + { + public new static PartyFramesPlayerStatusConfig DefaultConfig() + { + var config = new PartyFramesPlayerStatusConfig(); + config.Label.Enabled = false; + + return config; + } + + [Checkbox("Hide Name When Showing Status")] + [Order(5)] + public bool HideName = false; + + [NestedConfig("Icon", 10)] + public IconConfig Icon = new IconConfig( + new Vector2(0, 5), + new Vector2(16, 16), + DrawAnchor.Top, + DrawAnchor.Top + ); + + [NestedConfig("Label", 15)] + public LabelConfig Label = new LabelConfig(Vector2.Zero, "", DrawAnchor.Center, DrawAnchor.Center); + } + + [Exportable(false)] + public class PartyFramesReadyCheckStatusConfig : PluginConfigObject + { + public new static PartyFramesReadyCheckStatusConfig DefaultConfig() => new PartyFramesReadyCheckStatusConfig(); + + [Checkbox("Hide Name When Showing Status")] + [Order(5)] + public bool HideName = false; + + [DragInt("Duration (seconds)", min = 1, max = 60, help = "Determines for how long the icons will show after a ready check is finished.")] + [Order(6)] + public int Duration = 10; + + [NestedConfig("Icon", 10)] + public IconConfig Icon = new IconConfig( + new Vector2(0, 0), + new Vector2(24, 24), + DrawAnchor.TopRight, + DrawAnchor.TopRight + ); + } + + [Exportable(false)] + public class PartyFramesWhosTalkingConfig : PluginConfigObject + { + public new static PartyFramesWhosTalkingConfig DefaultConfig() => new PartyFramesWhosTalkingConfig(); + + [Checkbox("Replace Role/Job Icon when active")] + [Order(5)] + public bool ReplaceRoleJobIcon = false; + + [Checkbox("Show Speaking State", spacing = true)] + [Order(10)] + public bool ShowSpeaking = true; + + [Checkbox("Show Muted State")] + [Order(10)] + public bool ShowMuted = true; + + [Checkbox("Show Deafened State")] + [Order(10)] + public bool ShowDeafened = true; + + [NestedConfig("Icon", 20)] + public IconConfig Icon = new IconConfig( + new Vector2(0, 0), + new Vector2(24, 24), + DrawAnchor.TopRight, + DrawAnchor.TopRight + ); + + [Checkbox("Change Health Bar Border when active", spacing = true, help = "Enabling this will override other border settings!")] + [Order(30)] + public bool ChangeBorders = false; + + [DragInt("Border Thickness", min = 1, max = 10)] + [Order(31, collapseWith = nameof(ChangeBorders))] + public int BorderThickness = 1; + + [ColorEdit4("Speaking Border Color")] + [Order(32, collapseWith = nameof(ChangeBorders))] + public PluginConfigColor SpeakingBorderColor = PluginConfigColor.FromHex(0xFF40BB40); + + [ColorEdit4("Muted Border Color")] + [Order(33, collapseWith = nameof(ChangeBorders))] + public PluginConfigColor MutedBorderColor = PluginConfigColor.FromHex(0xFF008080); + + [ColorEdit4("Deafened Border Color")] + [Order(34, collapseWith = nameof(ChangeBorders))] + public PluginConfigColor DeafenedBorderColor = PluginConfigColor.FromHex(0xFFFF4444); + + public bool EnabledForState(WhosTalkingState state) + { + switch (state) + { + case WhosTalkingState.Speaking: return ShowSpeaking; + case WhosTalkingState.Muted: return ShowMuted; + case WhosTalkingState.Deafened: return ShowDeafened; + } + + return false; + } + + public PluginConfigColor? ColorForState(WhosTalkingState state) + { + if (state == WhosTalkingState.Speaking && ShowSpeaking) { return SpeakingBorderColor; } + if (state == WhosTalkingState.Muted && ShowMuted) { return MutedBorderColor; } + if (ShowDeafened) { return DeafenedBorderColor; } + + return null; + } + } + + [Exportable(false)] + [Section("Party Frames", true)] + [SubSection("Buffs", 0)] + public class PartyFramesBuffsConfig : PartyFramesStatusEffectsListConfig + { + public new static PartyFramesBuffsConfig DefaultConfig() + { + var durationConfig = new LabelConfig(new Vector2(0, -4), "", DrawAnchor.Bottom, DrawAnchor.Center); + var stacksConfig = new LabelConfig(new Vector2(-3, 4), "", DrawAnchor.TopRight, DrawAnchor.Center); + stacksConfig.Color = new(Vector4.UnitW); + stacksConfig.OutlineColor = new(Vector4.One); + + var iconConfig = new StatusEffectIconConfig(durationConfig, stacksConfig); + iconConfig.DispellableBorderConfig.Enabled = false; + iconConfig.Size = new Vector2(24, 24); + + var pos = new Vector2(-2, 2); + var size = new Vector2(iconConfig.Size.X * 4 + 6, iconConfig.Size.Y); + + var config = new PartyFramesBuffsConfig(DrawAnchor.TopRight, pos, size, true, false, false, GrowthDirections.Left | GrowthDirections.Down, iconConfig); + config.Limit = 4; + + return config; + } + + public PartyFramesBuffsConfig(DrawAnchor anchor, Vector2 position, Vector2 size, bool showBuffs, bool showDebuffs, bool showPermanentEffects, + GrowthDirections growthDirections, StatusEffectIconConfig iconConfig) + : base(anchor, position, size, showBuffs, showDebuffs, showPermanentEffects, growthDirections, iconConfig) + { + } + } + + [Exportable(false)] + [Section("Party Frames", true)] + [SubSection("Debuffs", 0)] + public class PartyFramesDebuffsConfig : PartyFramesStatusEffectsListConfig + { + public new static PartyFramesDebuffsConfig DefaultConfig() + { + var durationConfig = new LabelConfig(new Vector2(0, -4), "", DrawAnchor.Bottom, DrawAnchor.Center); + var stacksConfig = new LabelConfig(new Vector2(-3, 4), "", DrawAnchor.TopRight, DrawAnchor.Center); + stacksConfig.Color = new(Vector4.UnitW); + stacksConfig.OutlineColor = new(Vector4.One); + + var iconConfig = new StatusEffectIconConfig(durationConfig, stacksConfig); + iconConfig.Size = new Vector2(24, 24); + + var pos = new Vector2(-2, -2); + var size = new Vector2(iconConfig.Size.X * 4 + 6, iconConfig.Size.Y); + + var config = new PartyFramesDebuffsConfig(DrawAnchor.BottomRight, pos, size, false, true, false, GrowthDirections.Left | GrowthDirections.Up, iconConfig); + config.Limit = 4; + + return config; + } + + public PartyFramesDebuffsConfig(DrawAnchor anchor, Vector2 position, Vector2 size, bool showBuffs, bool showDebuffs, bool showPermanentEffects, + GrowthDirections growthDirections, StatusEffectIconConfig iconConfig) + : base(anchor, position, size, showBuffs, showDebuffs, showPermanentEffects, growthDirections, iconConfig) + { + } + } + + public class PartyFramesStatusEffectsListConfig : StatusEffectsListConfig + { + [Anchor("Health Bar Anchor")] + [Order(4)] + public DrawAnchor HealthBarAnchor = DrawAnchor.BottomLeft; + + public PartyFramesStatusEffectsListConfig(DrawAnchor anchor, Vector2 position, Vector2 size, bool showBuffs, bool showDebuffs, bool showPermanentEffects, + GrowthDirections growthDirections, StatusEffectIconConfig iconConfig) + : base(position, size, showBuffs, showDebuffs, showPermanentEffects, growthDirections, iconConfig) + { + HealthBarAnchor = anchor; + } + } + + [Disableable(false)] + [Exportable(false)] + [Section("Party Frames", true)] + [SubSection("Trackers", 0)] + public class PartyFramesTrackersConfig : PluginConfigObject + { + public new static PartyFramesTrackersConfig DefaultConfig() { return new PartyFramesTrackersConfig(); } + + [NestedConfig("Raise Tracker", 10, separator = false)] + public PartyFramesRaiseTrackerConfig Raise = new PartyFramesRaiseTrackerConfig(); + + [NestedConfig("Invulnerabilities Tracker", 15)] + public PartyFramesInvulnTrackerConfig Invuln = new PartyFramesInvulnTrackerConfig(); + + [NestedConfig("Cleanse Tracker", 15)] + public PartyFramesCleanseTrackerConfig Cleanse = new PartyFramesCleanseTrackerConfig(); + } + + [Exportable(false)] + public class PartyFramesRaiseTrackerConfig : PluginConfigObject + { + public new static PartyFramesRaiseTrackerConfig DefaultConfig() { return new PartyFramesRaiseTrackerConfig(); } + + [Checkbox("Hide Name When Raised")] + [Order(10)] + public bool HideNameWhenRaised = true; + + [Checkbox("Keep Icon After Cast Finishes")] + [Order(15)] + public bool KeepIconAfterCastFinishes = true; + + [Checkbox("Change Background Color When Raised", spacing = true)] + [Order(20)] + public bool ChangeBackgroundColorWhenRaised = true; + + [ColorEdit4("Raise Background Color")] + [Order(25, collapseWith = nameof(ChangeBackgroundColorWhenRaised))] + public PluginConfigColor BackgroundColor = new(new Vector4(211f / 255f, 235f / 255f, 215f / 245f, 50f / 100f)); + + [Checkbox("Change Border Color When Raised", spacing = true)] + [Order(30)] + public bool ChangeBorderColorWhenRaised = true; + + [ColorEdit4("Raise Border Color")] + [Order(35, collapseWith = nameof(ChangeBorderColorWhenRaised))] + public PluginConfigColor BorderColor = new(new Vector4(47f / 255f, 169f / 255f, 215f / 255f, 100f / 100f)); + + [NestedConfig("Icon", 50)] + public IconWithLabelConfig Icon = new IconWithLabelConfig( + new Vector2(0, 0), + new Vector2(50, 50), + DrawAnchor.Center, + DrawAnchor.Center + ); + } + + [Exportable(false)] + public class PartyFramesInvulnTrackerConfig : PluginConfigObject + { + public new static PartyFramesInvulnTrackerConfig DefaultConfig() { return new PartyFramesInvulnTrackerConfig(); } + + [Checkbox("Hide Name When Invuln is Up")] + [Order(10)] + public bool HideNameWhenInvuln = true; + + [Checkbox("Change Background Color When Invuln is Up", spacing = true)] + [Order(15)] + public bool ChangeBackgroundColorWhenInvuln = true; + + [ColorEdit4("Invuln Background Color")] + [Order(20, collapseWith = nameof(ChangeBackgroundColorWhenInvuln))] + public PluginConfigColor BackgroundColor = new(new Vector4(211f / 255f, 235f / 255f, 215f / 245f, 50f / 100f)); + + [Checkbox("Walking Dead Custom Color")] + [Order(25, collapseWith = nameof(ChangeBackgroundColorWhenInvuln))] + public bool UseCustomWalkingDeadColor = true; + + [ColorEdit4("Walking Dead Background Color")] + [Order(30, collapseWith = nameof(UseCustomWalkingDeadColor))] + public PluginConfigColor WalkingDeadBackgroundColor = new(new Vector4(158f / 255f, 158f / 255f, 158f / 255f, 50f / 100f)); + + [NestedConfig("Icon", 50)] + public IconWithLabelConfig Icon = new IconWithLabelConfig( + new Vector2(0, 0), + new Vector2(50, 50), + DrawAnchor.Center, + DrawAnchor.Center + ); + } + + public class PartyFramesTrackerConfigConverter : PluginConfigObjectConverter + { + public PartyFramesTrackerConfigConverter() + { + SameTypeFieldConverter pos = new SameTypeFieldConverter("Icon.Position", Vector2.Zero); + FieldConvertersMap.Add("Position", pos); + + SameTypeFieldConverter size = new SameTypeFieldConverter("Icon.Size", new Vector2(50, 50)); + FieldConvertersMap.Add("IconSize", size); + + SameTypeFieldConverter anchor = new SameTypeFieldConverter("Icon.Anchor", DrawAnchor.Center); + FieldConvertersMap.Add("Anchor", anchor); + + SameTypeFieldConverter frameAnchor = new SameTypeFieldConverter("Icon.FrameAnchor", DrawAnchor.Center); + FieldConvertersMap.Add("HealthBarAnchor", frameAnchor); + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(PartyFramesRaiseTrackerConfig) || + objectType == typeof(PartyFramesInvulnTrackerConfig); + } + } + + [DisableParentSettings("Position", "Strata")] + [Exportable(false)] + public class PartyFramesCleanseTrackerConfig : MovablePluginConfigObject + { + public new static PartyFramesCleanseTrackerConfig DefaultConfig() { return new PartyFramesCleanseTrackerConfig(); } + + [Checkbox("Show only on jobs with cleanses", spacing = true)] + [Order(10)] + public bool CleanseJobsOnly = true; + + [Checkbox("Change Health Bar Color ", spacing = true)] + [Order(15)] + public bool ChangeHealthBarCleanseColor = true; + + [ColorEdit4("Health Bar Color")] + [Order(20, collapseWith = nameof(ChangeHealthBarCleanseColor))] + public PluginConfigColor HealthBarColor = new(new Vector4(255f / 255f, 0f / 255f, 104f / 255f, 100f / 100f)); + + [Checkbox("Change Border Color", spacing = true)] + [Order(25)] + public bool ChangeBorderCleanseColor = true; + + [ColorEdit4("Border Color")] + [Order(30, collapseWith = nameof(ChangeBorderCleanseColor))] + public PluginConfigColor BorderColor = new(new Vector4(255f / 255f, 0f / 255f, 104f / 255f, 100f / 100f)); + } + + [Exportable(false)] + [DisableParentSettings("Anchor")] + [Section("Party Frames", true)] + [SubSection("Cooldowns", 0)] + public class PartyFramesCooldownListConfig : AnchorablePluginConfigObject + { + public new static PartyFramesCooldownListConfig DefaultConfig() + { + PartyFramesCooldownListConfig config = new PartyFramesCooldownListConfig(); + config.Position = new Vector2(-2, 0); + config.Size = new Vector2(40 * 8 + 6, 40); + + return config; + } + + [Anchor("Health Bar Anchor")] + [Order(3)] + public DrawAnchor HealthBarAnchor = DrawAnchor.Left; + + [Checkbox("Tooltips", spacing = true)] + [Order(20)] + public bool ShowTooltips = true; + + [Checkbox("Preview", isMonitored = true)] + [Order(21)] + public bool Preview; + + [DragInt2("Icon Size", min = 1, max = 4000, spacing = true)] + [Order(30)] + public Vector2 IconSize = new Vector2(40, 40); + + [DragInt2("Icon Padding", min = 0, max = 500)] + [Order(31)] + public Vector2 IconPadding = new(4, 4); + + [Checkbox("Fill Rows First")] + [Order(32)] + 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(33)] + public int Directions = 3; // left & up + + [Checkbox("Show Border", spacing = true)] + [Order(35)] + public bool DrawBorder = true; + + [ColorEdit4("Border Color")] + [Order(36, collapseWith = nameof(DrawBorder))] + public PluginConfigColor BorderColor = new PluginConfigColor(new Vector4(0f / 255f, 0f / 255f, 0f / 255f, 100f / 100f)); + + [DragInt("Border Thickness", min = 1, max = 10)] + [Order(37, collapseWith = nameof(DrawBorder))] + public int BorderThickness = 1; + + [Checkbox("Change Icon Border When Active")] + [Order(45, collapseWith = nameof(DrawBorder))] + public bool ChangeIconBorderWhenActive = true; + + [ColorEdit4("Icon Active Border Color")] + [Order(46, collapseWith = nameof(ChangeIconBorderWhenActive))] + public PluginConfigColor IconActiveBorderColor = new PluginConfigColor(new Vector4(255f / 255f, 200f / 255f, 35f / 255f, 100f / 100f)); + + [DragInt("Icon Active Border Thickness", min = 1, max = 10)] + [Order(47, collapseWith = nameof(ChangeIconBorderWhenActive))] + public int IconActiveBorderThickness = 3; + + [Checkbox("Change Label Color When Active", spacing = true)] + [Order(50)] + public bool ChangeLabelsColorWhenActive = false; + + [ColorEdit4("Label Active Color")] + [Order(51, collapseWith = nameof(ChangeLabelsColorWhenActive))] + public PluginConfigColor LabelsActiveColor = new PluginConfigColor(new Vector4(255f / 255f, 200f / 255f, 35f / 255f, 100f / 100f)); + + [NestedConfig("Time Label", 80)] + public PartyCooldownTimeLabelConfig TimeLabel = new PartyCooldownTimeLabelConfig(new Vector2(0, 0), "", DrawAnchor.Center, DrawAnchor.Center) { NumberFormat = 1 }; + } +} diff --git a/Interface/Party/PartyFramesCooldownListHud.cs b/Interface/Party/PartyFramesCooldownListHud.cs new file mode 100644 index 0000000..d29ed74 --- /dev/null +++ b/Interface/Party/PartyFramesCooldownListHud.cs @@ -0,0 +1,295 @@ +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Plugin.Services; +using HSUI.Config; +using HSUI.Enums; +using HSUI.Helpers; +using HSUI.Interface.GeneralElements; +using HSUI.Interface.PartyCooldowns; +using Dalamud.Bindings.ImGui; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace HSUI.Interface.Party +{ + public class PartyFramesCooldownListHud : ParentAnchoredDraggableHudElement, IHudElementWithActor, IHudElementWithAnchorableParent, IHudElementWithPreview + { + private PartyFramesCooldownListConfig Config => (PartyFramesCooldownListConfig)_config; + private PartyCooldownsDataConfig _dataConfig = null!; + + private LabelHud _timeLabel; + private bool _needsUpdate = true; + private LayoutInfo _layoutInfo; + + private List _cooldowns = new List(); + private List? _fakeCooldowns = null; + + public IGameObject? Actor { get; set; } + + protected override bool AnchorToParent => true; + protected override DrawAnchor ParentAnchor => Config is PartyFramesCooldownListConfig config ? config.HealthBarAnchor : DrawAnchor.Center; + + public PartyFramesCooldownListHud(PartyFramesCooldownListConfig config, string? displayName = null) : base(config, displayName) + { + _timeLabel = new LabelHud(config.TimeLabel); + + ConfigurationManager.Instance.ResetEvent += OnConfigReset; + PartyCooldownsManager.Instance.CooldownsChangedEvent += OnCooldownsChanged; + _config.ValueChangeEvent += OnConfigPropertyChanged; + + OnConfigReset(ConfigurationManager.Instance); + } + + private void OnConfigReset(ConfigurationManager sender) + { + if (_dataConfig != null) + _dataConfig.CooldownsDataChangedEvent -= OnCooldownsDataChanged; + _dataConfig = ConfigurationManager.Instance.GetConfigObject(); + _dataConfig.CooldownsDataChangedEvent += OnCooldownsDataChanged; + } + + protected override void InternalDispose() + { + ConfigurationManager.Instance?.ResetEvent -= OnConfigReset; + _config.ValueChangeEvent -= OnConfigPropertyChanged; + _dataConfig?.CooldownsDataChangedEvent -= OnCooldownsDataChanged; + PartyCooldownsManager.Instance?.CooldownsChangedEvent -= OnCooldownsChanged; + } + + private void OnConfigPropertyChanged(object? sender, OnChangeBaseArgs args) + { + if (args.PropertyName == "Preview") + { + UpdatePreview(); + } + } + + private unsafe void UpdatePreview() + { + if (!Config.Preview) + { + _fakeCooldowns = null; + return; + } + + var RNG = new Random((int)ImGui.GetTime()); + + _fakeCooldowns = new List(); + + for (int i = 0; i < 10; i++) + { + int index = RNG.Next(0, _dataConfig.Cooldowns.Count); + + PartyCooldown cooldown = new PartyCooldown(_dataConfig.Cooldowns[index], 0, 90, null); + + int rng = RNG.Next(100); + if (rng > 80) + { + cooldown.LastTimeUsed = ImGui.GetTime() - 30; + } + else if (rng > 50) + { + cooldown.LastTimeUsed = ImGui.GetTime() + 1; + } + + _fakeCooldowns.Add(cooldown); + } + } + + public void StopPreview() + { + Config.Preview = false; + UpdatePreview(); + } + + private void OnCooldownsDataChanged(PartyCooldownsDataConfig sender) + { + _needsUpdate = true; + } + + private void OnCooldownsChanged(PartyCooldownsManager sender) + { + _needsUpdate = true; + } + + private void UpdateCooldowns() + { + _cooldowns.Clear(); + + if (Actor == null || PartyCooldownsManager.Instance?.CooldownsMap == null) { return; } + + if (PartyCooldownsManager.Instance.CooldownsMap.TryGetValue((uint)Actor.GameObjectId, out Dictionary? dict) && dict != null) + { + _cooldowns = dict.Values.Where(o => o.Data.IsEnabledForPartyFrames()).ToList(); + } + + _cooldowns.Sort((a, b) => + { + int aOrder = a.Data.Column * 1000 + a.Data.Priority; + int bOrder = b.Data.Column * 1000 + b.Data.Priority; + + return aOrder.CompareTo(bOrder); + }); + + _needsUpdate = false; + } + + private void CalculateLayout(uint count) + { + if (count <= 0) { return; } + + _layoutInfo = LayoutHelper.CalculateLayout( + Config.Size, + Config.IconSize, + count, + Config.IconPadding, + LayoutHelper.GetFillsRowsFirst(Config.FillRowsFirst, LayoutHelper.GrowthDirectionsFromIndex(Config.Directions)) + ); + } + + public override void DrawChildren(Vector2 origin) + { + if (!Config.Enabled) { return; } + + if (_needsUpdate) + { + UpdateCooldowns(); + } + + List list = _fakeCooldowns != null ? _fakeCooldowns : _cooldowns; + + if (list.Count == 0) { return; } + + // 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(); + + // calculate icon positions + uint count = (uint)list.Count; + CalculateLayout(count); + var (iconPositions, minPos, maxPos) = LayoutHelper.CalculateIconPositions( + growthDirections, + count, + position, + Config.Size, + Config.IconSize, + 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, false, (drawList) => + { + // area + if (Config.Preview) + { + drawList.AddRectFilled(areaPos, areaPos + Config.Size, 0x88000000); + } + + for (int i = 0; i < count; i++) + { + Vector2 iconPos = iconPositions[i]; + PartyCooldown cooldown = list[i]; + + float cooldownTime = cooldown.CooldownTimeRemaining(); + float effectTime = cooldown.EffectTimeRemaining(); + + // icon + bool recharging = effectTime == 0 && cooldownTime > 0; + uint color = recharging ? 0xAAFFFFFF : 0xFFFFFFFF; + bool shouldDrawCooldown = ClipRectsHelper.Instance.GetClipRectForArea(iconPos, Config.IconSize) == null; + + DrawHelper.DrawIcon(cooldown.Data.IconId, iconPos, Config.IconSize, false, color, drawList); + + if (shouldDrawCooldown && effectTime == 0 && cooldownTime > 0) + { + DrawHelper.DrawIconCooldown(iconPos, Config.IconSize, cooldownTime, cooldown.Data.CooldownDuration, drawList); + } + + // border + if (Config.DrawBorder) + { + bool active = effectTime > 0 && Config.ChangeIconBorderWhenActive; + uint iconBorderColor = active ? Config.IconActiveBorderColor.Base : Config.BorderColor.Base; + int thickness = active ? Config.IconActiveBorderThickness : Config.BorderThickness; + drawList.AddRect(iconPos, iconPos + Config.IconSize, iconBorderColor, 0, ImDrawFlags.None, thickness); + } + } + }); + }); + + PartyCooldown? hoveringCooldown = 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]; + PartyCooldown cooldown = list[i]; + + float cooldownTime = cooldown.CooldownTimeRemaining(); + float effectTime = cooldown.EffectTimeRemaining(); + + PluginConfigColor? labelColor = effectTime > 0 && Config.ChangeLabelsColorWhenActive ? Config.LabelsActiveColor : null; + + // time + AddDrawAction(Config.TimeLabel.StrataLevel, () => + { + PluginConfigColor realColor = Config.TimeLabel.Color; + Config.TimeLabel.Color = labelColor ?? realColor; + Config.TimeLabel.SetText(""); + + if (effectTime > 0) + { + if (Config.TimeLabel.ShowEffectDuration) + { + Config.TimeLabel.SetValue(effectTime); + } + } + else if (cooldownTime > 0) + { + if (Config.TimeLabel.ShowRemainingCooldown) + { + Config.TimeLabel.SetText(Utils.DurationToString(cooldownTime, Config.TimeLabel.NumberFormat)); + } + } + + _timeLabel.Draw(iconPos, Config.IconSize, character); + Config.TimeLabel.Color = realColor; + }); + + // tooltips / interaction + if (ImGui.IsMouseHoveringRect(iconPos, iconPos + Config.IconSize)) + { + hoveringCooldown = cooldown; + } + } + + if (hoveringCooldown != null) + { + // tooltip + if (Config.ShowTooltips) + { + TooltipsHelper.Instance.ShowTooltipOnCursor( + hoveringCooldown.TooltipText(), + hoveringCooldown.Data.Name, + hoveringCooldown.Data.ActionId + ); + } + } + } + } +} diff --git a/Interface/Party/PartyFramesHud.cs b/Interface/Party/PartyFramesHud.cs new file mode 100644 index 0000000..12bf708 --- /dev/null +++ b/Interface/Party/PartyFramesHud.cs @@ -0,0 +1,465 @@ +using Dalamud.Game.ClientState.Objects.Types; +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 System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; + +namespace HSUI.Interface.Party +{ + public class PartyFramesHud : DraggableHudElement, IHudElementWithMouseOver, IHudElementWithPreview, IHudElementWithVisibilityConfig + { + private PartyFramesConfig Config => (PartyFramesConfig)_config; + public VisibilityConfig VisibilityConfig => Config.VisibilityConfig; + private PartyFramesConfigs Configs; + + private Vector2 _contentMargin = new Vector2(2, 2); + private static readonly int MaxMemberCount = 9; // 8 players + chocobo + + // layout + private Vector2 _origin; + private LayoutInfo _layoutInfo; + private uint _memberCount = 0; + private bool _layoutDirty = true; + + private readonly List bars; + private LabelHud _titleLabelHud; + + private bool Locked => !ConfigurationManager.Instance.IsConfigWindowOpened; + + + public PartyFramesHud(PartyFramesConfig config, string displayName) : base(config, displayName) + { + Configs = PartyFramesConfigs.GetConfigs(); + + config.ValueChangeEvent += OnLayoutPropertyChanged; + Configs.HealthBar.ValueChangeEvent += OnLayoutPropertyChanged; + Configs.HealthBar.ColorsConfig.ValueChangeEvent += OnLayoutPropertyChanged; + + bars = new List(MaxMemberCount); + for (int i = 0; i < bars.Capacity; i++) + { + PartyFramesBar bar = new PartyFramesBar("DelvUI_partyFramesBar" + i, Configs); + bar.OpenContextMenuEvent += OnOpenContextMenu; + + bars.Add(bar); + } + + _titleLabelHud = new LabelHud(config.ShowPartyTitleConfig); + + PartyManager.Instance.MembersChangedEvent += OnMembersChanged; + UpdateBars(Vector2.Zero); + } + + protected override void InternalDispose() + { + foreach (var bar in bars) + { + try { bar.Dispose(); } + catch (Exception ex) { Plugin.Logger.Error($"Error disposing PartyFramesBar: {ex.Message}"); } + } + bars.Clear(); + + _config.ValueChangeEvent -= OnLayoutPropertyChanged; + Configs.HealthBar.ValueChangeEvent -= OnLayoutPropertyChanged; + Configs.HealthBar.ColorsConfig.ValueChangeEvent -= OnLayoutPropertyChanged; + PartyManager.Instance.MembersChangedEvent -= OnMembersChanged; + } + + private unsafe void OnOpenContextMenu(PartyFramesBar bar) + { + if (bar.Member == null || Plugin.ObjectTable.LocalPlayer == null) + { + return; + } + + if (PartyManager.Instance.PartyListAddon == null || PartyManager.Instance.HudAgent == IntPtr.Zero) + { + return; + } + + int addonId = PartyManager.Instance.PartyListAddon->AtkUnitBase.Id; + AgentModule.Instance()->GetAgentHUD()->OpenContextMenuFromPartyAddon(addonId, bar.Member.Index); + } + + private void OnLayoutPropertyChanged(object sender, OnChangeBaseArgs args) + { + if (args.PropertyName == "Size" || + args.PropertyName == "FillRowsFirst" || + args.PropertyName == "BarsAnchor" || + args.PropertyName == "Padding" || + args.PropertyName == "Rows" || + args.PropertyName == "Columns") + { + _layoutDirty = true; + } + } + + private void OnMembersChanged(PartyManager sender) + { + UpdateBars(_origin); + } + + public void UpdateBars(Vector2 origin) + { + uint memberCount = PartyManager.Instance.MemberCount; + uint row = 0; + uint col = 0; + + for (int i = 0; i < bars.Count; i++) + { + PartyFramesBar bar = bars[i]; + if (i >= memberCount) + { + bar.Visible = false; + continue; + } + + // update bar + IPartyFramesMember member = PartyManager.Instance.SortedGroupMembers.ElementAt(i); + bar.Member = member; + bar.Visible = true; + + // anchor and position + CalculateBarPosition(origin, Size, out var x, out var y); + bar.Position = new Vector2( + x + Configs.HealthBar.Size.X * col + (Configs.HealthBar.Padding.X - 1) * col, + y + Configs.HealthBar.Size.Y * row + (Configs.HealthBar.Padding.Y - 1) * row + ); + + // layout + if (Config.FillRowsFirst) + { + col = col + 1; + if (col >= _layoutInfo.TotalColCount) + { + col = 0; + row = row + 1; + } + } + else + { + row = row + 1; + if (row >= _layoutInfo.TotalRowCount) + { + row = 0; + col = col + 1; + } + } + } + } + + private void CalculateBarPosition(Vector2 position, Vector2 spaceSize, out float x, out float y) + { + x = position.X; + y = position.Y; + + if (Config.BarsAnchor == DrawAnchor.Top || + Config.BarsAnchor == DrawAnchor.Center || + Config.BarsAnchor == DrawAnchor.Bottom) + { + x += (spaceSize.X - _layoutInfo.ContentSize.X) / 2f; + } + else if (Config.BarsAnchor == DrawAnchor.TopRight || + Config.BarsAnchor == DrawAnchor.Right || + Config.BarsAnchor == DrawAnchor.BottomRight) + { + x += spaceSize.X - _layoutInfo.ContentSize.X; + } + + if (Config.BarsAnchor == DrawAnchor.Left || + Config.BarsAnchor == DrawAnchor.Center || + Config.BarsAnchor == DrawAnchor.Right) + { + y += (spaceSize.Y - _layoutInfo.ContentSize.Y) / 2f; + } + else if (Config.BarsAnchor == DrawAnchor.BottomLeft || + Config.BarsAnchor == DrawAnchor.Bottom || + Config.BarsAnchor == DrawAnchor.BottomRight) + { + y += spaceSize.Y - _layoutInfo.ContentSize.Y; + } + } + + private void UpdateBarsPosition(Vector2 delta) + { + foreach (PartyFramesBar bar in bars) + { + bar.Position = bar.Position + delta; + } + } + + public void StopPreview() + { + Config.Preview = false; + PartyManager.Instance?.UpdatePreview(); + + foreach (PartyFramesBar bar in bars) + { + bar.StopPreview(); + } + } + + protected override (List, List) ChildrenPositionsAndSizes() + { + return (new List() { Config.Position + Size / 2f }, new List() { Size }); + } + + public void StopMouseover() + { + foreach (PartyFramesBar bar in bars) + { + bar.StopMouseover(); + } + } + + private Vector2 Size => new Vector2( + Config.Columns * Configs.HealthBar.Size.X + (Config.Columns - 1) * Configs.HealthBar.Padding.X, + Config.Rows * Configs.HealthBar.Size.Y + (Config.Rows - 1) * Configs.HealthBar.Padding.Y + ); + + private void UpdateLayout(Vector2 origin) + { + Vector2 contentStartPos = origin + Config.Position; + uint count = PartyManager.Instance.MemberCount; + + if (_layoutDirty || _memberCount != count) + { + _layoutInfo = LayoutHelper.CalculateLayout( + Size, + Configs.HealthBar.Size, + count, + Configs.HealthBar.Padding, + Config.FillRowsFirst + ); + UpdateBars(contentStartPos); + } + else if (_origin != contentStartPos) + { + UpdateBarsPosition(contentStartPos - _origin); + } + + _layoutDirty = false; + _origin = contentStartPos; + _memberCount = count; + } + + public override void DrawChildren(Vector2 origin) + { + if (!_config.Enabled) + { + return; + } + + // area bg + if (Config.Preview) + { + AddDrawAction(StrataLevel.LOWEST, () => + { + ImDrawListPtr drawList = ImGui.GetWindowDrawList(); + Vector2 bgPos = origin + Config.Position - _contentMargin; + Vector2 bgSize = Size + _contentMargin * 2; + + drawList.AddRectFilled(bgPos, bgPos + bgSize, 0x66000000); + drawList.AddRect(bgPos, bgPos + bgSize, 0x66FFFFFF); + }); + } + + uint count = PartyManager.Instance.MemberCount; + if (count < 1) + { + return; + } + + UpdateLayout(origin); + + // draw bars + // check borders to determine the order in which the bars are drawn + // which is necessary for grid-like party frames + + IGameObject? target = Plugin.TargetManager.SoftTarget ?? Plugin.TargetManager.Target; + int targetIndex = -1; + int enmityLeaderIndex = -1; + int enmitySecondIndex = -1; + + List raisedIndexes = new List(); + List cleanseIndexes = new List(); + List whosTalkingIndexes = new List(); + + for (int i = 0; i < count; i++) + { + IPartyFramesMember? member = bars[i].Member; + + if (member != null) + { + // whos talking + if (Configs.Icons.WhosTalking.ChangeBorders && member.WhosTalkingState != WhosTalkingState.None) + { + whosTalkingIndexes.Add(i); + continue; + } + + // target + if (target != null && member.ObjectId == target.GameObjectId) + { + targetIndex = i; + continue; + } + + // cleanse + bool cleanseCheck = true; + if (Configs.Trackers.Cleanse.CleanseJobsOnly) + { + cleanseCheck = Utils.IsOnCleanseJob(); + } + + if (Configs.Trackers.Cleanse.Enabled && Configs.Trackers.Cleanse.ChangeBorderCleanseColor && member.HasDispellableDebuff && cleanseCheck) + { + cleanseIndexes.Add(i); + continue; + } + + // raise + if (Configs.Trackers.Raise.Enabled && Configs.Trackers.Raise.ChangeBorderColorWhenRaised && member.RaiseTime.HasValue) + { + raisedIndexes.Add(i); + continue; + } + + // enmity + if (Configs.HealthBar.ColorsConfig.ShowEnmityBorderColors) + { + if (member.EnmityLevel == EnmityLevel.Leader) + { + enmityLeaderIndex = i; + continue; + } + else if (Configs.HealthBar.ColorsConfig.ShowSecondEnmity && member.EnmityLevel == EnmityLevel.Second && + (count > 4 || !Configs.HealthBar.ColorsConfig.HideSecondEnmityInLightParties)) + { + enmitySecondIndex = i; + continue; + } + } + } + + // no special border + AddDrawActions(bars[i].GetBarDrawActions(origin)); + } + + // special colors for borders + + // 2nd enmity + if (enmitySecondIndex >= 0) + { + AddDrawActions(bars[enmitySecondIndex].GetBarDrawActions(origin, Configs.HealthBar.ColorsConfig.EnmitySecondBordercolor)); + } + + // 1st enmity + if (enmityLeaderIndex >= 0) + { + AddDrawActions(bars[enmityLeaderIndex].GetBarDrawActions(origin, Configs.HealthBar.ColorsConfig.EnmityLeaderBordercolor)); + } + + // raise + foreach (int index in raisedIndexes) + { + AddDrawActions(bars[index].GetBarDrawActions(origin, Configs.Trackers.Raise.BorderColor)); + } + + // target + if (targetIndex >= 0) + { + AddDrawActions(bars[targetIndex].GetBarDrawActions(origin, Configs.HealthBar.ColorsConfig.TargetBordercolor)); + } + + // cleanseable debuff + foreach (int index in cleanseIndexes) + { + AddDrawActions(bars[index].GetBarDrawActions(origin, Configs.Trackers.Cleanse.BorderColor)); + } + + // whos talking + foreach (int index in whosTalkingIndexes) + { + IPartyFramesMember? member = bars[index].Member; + if (member != null) + { + AddDrawActions(bars[index].GetBarDrawActions(origin, Configs.Icons.WhosTalking.ColorForState(member.WhosTalkingState))); + } + else + { + AddDrawActions(bars[index].GetBarDrawActions(origin)); + } + } + + // extra elements + foreach (PartyFramesBar bar in bars) + { + AddDrawActions(bar.GetElementsDrawActions(origin)); + } + + AddDrawAction(Config.ShowPartyTitleConfig.StrataLevel, () => + { + Config.ShowPartyTitleConfig.SetText(PartyManager.Instance.PartyTitle); + _titleLabelHud.Draw(origin + Config.Position); + }); + } + } + + #region utils + public struct PartyFramesConfigs + { + public PartyFramesHealthBarsConfig HealthBar; + public PartyFramesManaBarConfig ManaBar; + public PartyFramesCastbarConfig CastBar; + public PartyFramesIconsConfig Icons; + public PartyFramesBuffsConfig Buffs; + public PartyFramesDebuffsConfig Debuffs; + public PartyFramesTrackersConfig Trackers; + public PartyFramesCooldownListConfig CooldownList; + + public PartyFramesConfigs( + PartyFramesHealthBarsConfig healthBar, + PartyFramesManaBarConfig manaBar, + PartyFramesCastbarConfig castBar, + PartyFramesIconsConfig icons, + PartyFramesBuffsConfig buffs, + PartyFramesDebuffsConfig debuffs, + PartyFramesTrackersConfig trackers, + PartyFramesCooldownListConfig cooldownList) + { + HealthBar = healthBar; + ManaBar = manaBar; + CastBar = castBar; + Icons = icons; + Buffs = buffs; + Debuffs = debuffs; + Trackers = trackers; + CooldownList = cooldownList; + } + + public static PartyFramesConfigs GetConfigs() + { + return new PartyFramesConfigs( + ConfigurationManager.Instance.GetConfigObject(), + ConfigurationManager.Instance.GetConfigObject(), + ConfigurationManager.Instance.GetConfigObject(), + ConfigurationManager.Instance.GetConfigObject(), + ConfigurationManager.Instance.GetConfigObject(), + ConfigurationManager.Instance.GetConfigObject(), + ConfigurationManager.Instance.GetConfigObject(), + ConfigurationManager.Instance.GetConfigObject() + ); + } + + + } + #endregion +} diff --git a/Interface/Party/PartyFramesInvulnTracker.cs b/Interface/Party/PartyFramesInvulnTracker.cs new file mode 100644 index 0000000..377ef62 --- /dev/null +++ b/Interface/Party/PartyFramesInvulnTracker.cs @@ -0,0 +1,111 @@ +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Game.ClientState.Statuses; +using HSUI.Config; +using HSUI.Helpers; +using System; +using System.Collections.Generic; + +namespace HSUI.Interface.Party +{ + public class InvulnStatus + { + public readonly uint InvulnIcon; + public readonly float InvulnTime; + public readonly uint InvulnId; + + public InvulnStatus(uint invulnIcon, float invulnTime, uint invulnId) + { + InvulnIcon = invulnIcon; + InvulnTime = invulnTime; + InvulnId = invulnId; + } + } + public class PartyFramesInvulnTracker : IDisposable + { + private PartyFramesInvulnTrackerConfig _config = null!; + + public PartyFramesInvulnTracker() + { + ConfigurationManager.Instance.ResetEvent += OnConfigReset; + OnConfigReset(ConfigurationManager.Instance); + } + + ~PartyFramesInvulnTracker() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + + ConfigurationManager.Instance.ResetEvent -= OnConfigReset; + } + + public void OnConfigReset(ConfigurationManager sender) + { + _config = ConfigurationManager.Instance.GetConfigObject().Invuln; + } + + public void Update(List partyMembers) + { + if (!_config.Enabled) + { + return; + } + + + foreach (var member in partyMembers) + { + + if (member.Character == null || member.ObjectId == 0) + { + member.InvulnStatus = null; + continue; + } + + if (member.Character is not IBattleChara battleChara || member.HP <= 0) + { + member.InvulnStatus = null; + continue; + } + + // check invuln buff + IStatus? tankInvuln = Utils.GetTankInvulnerabilityID(battleChara); + if (tankInvuln == null) + { + member.InvulnStatus = null; + continue; + } + + // apply invuln data based on buff + + member.InvulnStatus = new InvulnStatus(InvulnMap[tankInvuln.StatusId], tankInvuln.RemainingTime, tankInvuln.StatusId); + } + } + + #region invuln ids + //these need to be mapped instead + private static Dictionary InvulnMap = new Dictionary() + { + { 810, 003077 }, // LIVING DEAD + { 3255, 003077}, // UNDEAD REBIRTH + { 811, 003077 }, // WALKING DEAD + { 1302, 002502 }, // HALLOWED GROUND + { 82, 002502 }, // HALLOWED GROUND + { 409, 000266 }, // HOLMGANG + { 1836, 003416 } // SUPERBOLIDE + }; + + #endregion + } +} diff --git a/Interface/Party/PartyFramesMember.cs b/Interface/Party/PartyFramesMember.cs new file mode 100644 index 0000000..3478a1d --- /dev/null +++ b/Interface/Party/PartyFramesMember.cs @@ -0,0 +1,235 @@ +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Game.ClientState.Party; +using HSUI.Helpers; +using Dalamud.Bindings.ImGui; +using System; +using FFXIVClientStructs.FFXIV.Client.UI.Info; + +namespace HSUI.Interface.Party +{ + public enum EnmityLevel : byte + { + Leader = 1, + Second = 2, + Last = 255 + } + + public enum PartyMemberStatus : byte + { + None, + ViewingCutscene, + Offline, + Dead + } + + public unsafe class PartyFramesMember : IPartyFramesMember + { + protected IPartyMember? _partyMember = null; + private string _name = ""; + private uint _jobId = 0; + private uint _objectID = 0; + + public uint ObjectId => _partyMember != null ? _partyMember.EntityId : _objectID; + public ICharacter? Character { get; private set; } + public CrossRealmMember? CrossCharacter { get; private set; } + + public int Index { get; set; } + public int Order { get; set; } + public string Name => _partyMember != null ? _partyMember.Name.ToString() : (Character != null ? Character.Name.ToString() : _name); + public uint Level => _partyMember != null ? _partyMember.Level : (Character != null ? Character.Level : (uint)0); + public uint JobId => _partyMember != null ? _partyMember.ClassJob.RowId : (Character != null ? Character.ClassJob.RowId : _jobId); + public uint HP => _partyMember != null ? _partyMember.CurrentHP : (Character != null ? Character.CurrentHp : (uint)0); + public uint MaxHP => _partyMember != null ? _partyMember.MaxHP : (Character != null ? Character.MaxHp : (uint)0); + public uint MP => _partyMember != null ? _partyMember.CurrentMP : JobsHelper.CurrentPrimaryResource(Character); + public uint MaxMP => _partyMember != null ? _partyMember.MaxMP : JobsHelper.MaxPrimaryResource(Character); + public float Shield => Utils.ActorShieldValue(Character); + public EnmityLevel EnmityLevel { get; private set; } = EnmityLevel.Last; + public PartyMemberStatus Status { get; private set; } = PartyMemberStatus.None; + public ReadyCheckStatus ReadyCheckStatus { get; private set; } = ReadyCheckStatus.None; + public bool IsPartyLeader { get; private set; } = false; + public bool IsChocobo { get; private set; } = false; + public float? RaiseTime { get; set; } + public InvulnStatus? InvulnStatus { get; set; } + public bool HasDispellableDebuff { get; set; } = false; + public WhosTalkingState WhosTalkingState => WhosTalkingHelper.Instance?.GetUserState(Name) ?? WhosTalkingState.None; + + public PartyFramesMember(IPartyMember partyMember, int index, int order, EnmityLevel enmityLevel, PartyMemberStatus status, ReadyCheckStatus readyCheckStatus, bool isPartyLeader, bool isChocobo = false) + { + _partyMember = partyMember; + Index = index; + Order = order; + EnmityLevel = enmityLevel; + Status = status; + ReadyCheckStatus = readyCheckStatus; + IsPartyLeader = isPartyLeader; + IsChocobo = isChocobo; + + var gameObject = partyMember.GameObject; + if (gameObject is ICharacter character) + { + Character = character; + } + } + + public PartyFramesMember(ICharacter character, int index, int order, EnmityLevel enmityLevel, PartyMemberStatus status, ReadyCheckStatus readyCheckStatus, bool isPartyLeader, bool isChocobo = false) + { + Index = index; + Order = order; + EnmityLevel = enmityLevel; + Status = status; + ReadyCheckStatus = readyCheckStatus; + IsPartyLeader = isPartyLeader; + IsChocobo = isChocobo; + + _objectID = (uint)character.GameObjectId; + Character = character; + } + + public PartyFramesMember(uint objectId, int index, int order, EnmityLevel enmityLevel, PartyMemberStatus status, ReadyCheckStatus readyCheckStatus, bool isPartyLeader, bool isChocobo = false) + { + Index = index; + Order = order; + EnmityLevel = enmityLevel; + Status = status; + ReadyCheckStatus = readyCheckStatus; + IsPartyLeader = isPartyLeader; + IsChocobo = isChocobo; + + _objectID = objectId; + var gameObject = Plugin.ObjectTable.SearchById(ObjectId); + Character = gameObject is ICharacter ? (ICharacter)gameObject : null; + } + + public PartyFramesMember(CrossRealmMember member, int index, int order, PartyMemberStatus status, ReadyCheckStatus readyCheckStatus, bool isPartyLeader, bool isChocobo = false) + { + Index = index; + Order = order; + Status = status; + ReadyCheckStatus = readyCheckStatus; + IsPartyLeader = isPartyLeader; + IsChocobo = isChocobo; + + _objectID = (uint)member.EntityId; + CrossCharacter = member; + _name = member.NameString; + _jobId = member.ClassJobId; + } + + + public void Update(EnmityLevel enmityLevel, PartyMemberStatus status, ReadyCheckStatus readyCheckStatus, bool isPartyLeader, uint jobId = 0) + { + EnmityLevel = enmityLevel; + Status = status; + ReadyCheckStatus = readyCheckStatus; + IsPartyLeader = isPartyLeader; + + if (ObjectId == 0) + { + Character = null; + return; + } + + var gameObject = Plugin.ObjectTable.SearchById(ObjectId); + Character = gameObject is ICharacter ? (ICharacter)gameObject : null; + + if (jobId > 0) + { + _jobId = jobId; + } + else if (Character != null) + { + _jobId = Character.ClassJob.RowId; + } + + if (status == PartyMemberStatus.None && Character != null && MaxHP > 0 && HP <= 0) + { + Status = PartyMemberStatus.Dead; + } + } + } + + public class FakePartyFramesMember : IPartyFramesMember + { + public static readonly Random RNG = new Random((int)ImGui.GetTime()); + + public uint ObjectId => 0xE0000000; + public ICharacter? Character => null; + + public int Index { get; set; } + public int Order { get; set; } + public string Name { get; private set; } + public uint Level { get; private set; } + public uint JobId { get; private set; } + public uint HP { get; private set; } + public uint MaxHP { get; private set; } + public uint MP { get; private set; } + public uint MaxMP { get; private set; } + public float Shield { get; private set; } + public EnmityLevel EnmityLevel { get; private set; } + public PartyMemberStatus Status { get; private set; } + public ReadyCheckStatus ReadyCheckStatus { get; private set; } + public bool IsPartyLeader { get; } + public bool IsChocobo { get; } + public float? RaiseTime { get; set; } + public InvulnStatus? InvulnStatus { get; set; } + public bool HasDispellableDebuff { get; set; } + public WhosTalkingState WhosTalkingState { get; set; } + + public FakePartyFramesMember(int order) + { + Name = RNG.Next(0, 2) == 1 ? "Fake Name" : "FakeLonger MockedName"; + Index = order; + Order = order + 1; + Level = (uint)RNG.Next(1, 80); + JobId = (uint)RNG.Next(19, 41); + MaxHP = (uint)RNG.Next(90000, 150000); + HP = order == 2 || order == 3 ? 0 : (uint)(MaxHP * RNG.Next(50, 100) / 100f); + MaxMP = 10000; + MP = order == 2 || order == 3 ? 0 : (uint)(MaxMP * RNG.Next(100) / 100f); + Shield = order == 2 || order == 3 ? 0 : RNG.Next(30) / 100f; + EnmityLevel = order <= 1 ? (EnmityLevel)order + 1 : EnmityLevel.Last; + Status = order < 3 ? PartyMemberStatus.None : (order == 3 ? PartyMemberStatus.Dead : (PartyMemberStatus)RNG.Next(0, 3)); + ReadyCheckStatus = (ReadyCheckStatus)RNG.Next(0, 3); + IsPartyLeader = order == 0; + IsChocobo = RNG.Next(0, 8) == 1; + HasDispellableDebuff = RNG.Next(0, 2) == 1; + RaiseTime = order == 2 ? RNG.Next(0, 60) : null; + InvulnStatus = order == 0 ? new InvulnStatus(3077, RNG.Next(0, 10), 810) : null; + WhosTalkingState = (WhosTalkingState)RNG.Next(0, 4); + } + + public void Update(EnmityLevel enmityLevel, PartyMemberStatus status, ReadyCheckStatus readyCheckStatus, bool isPartyLeader, uint jobId = 0) + { + + } + } + + public interface IPartyFramesMember + { + public uint ObjectId { get; } + public ICharacter? Character { get; } + + public int Index { get; } + public int Order { get; } + public string Name { get; } + public uint Level { get; } + public uint JobId { get; } + public uint HP { get; } + public uint MaxHP { get; } + public uint MP { get; } + public uint MaxMP { get; } + public float Shield { get; } + public EnmityLevel EnmityLevel { get; } + public PartyMemberStatus Status { get; } + public ReadyCheckStatus ReadyCheckStatus { get; } + public bool IsPartyLeader { get; } + public bool IsChocobo { get; } + public float? RaiseTime { get; set; } + public InvulnStatus? InvulnStatus { get; set; } + public bool HasDispellableDebuff { get; set; } + + public WhosTalkingState WhosTalkingState { get; } + + public void Update(EnmityLevel enmityLevel, PartyMemberStatus status, ReadyCheckStatus readyCheckStatus, bool isPartyLeader, uint jobId = 0); + } +} diff --git a/Interface/Party/PartyFramesRaiseTracker.cs b/Interface/Party/PartyFramesRaiseTracker.cs new file mode 100644 index 0000000..5921826 --- /dev/null +++ b/Interface/Party/PartyFramesRaiseTracker.cs @@ -0,0 +1,185 @@ +using Dalamud.Game.ClientState.Objects.Types; +using HSUI.Config; +using HSUI.Helpers; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace HSUI.Interface.Party +{ + public class PartyFramesRaiseTracker : IDisposable + { + private PartyFramesRaiseTrackerConfig _config = null!; + + public PartyFramesRaiseTracker() + { + ConfigurationManager.Instance.ResetEvent += OnConfigReset; + OnConfigReset(ConfigurationManager.Instance); + } + + ~PartyFramesRaiseTracker() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + + ConfigurationManager.Instance.ResetEvent -= OnConfigReset; + } + + public void OnConfigReset(ConfigurationManager sender) + { + _config = sender.GetConfigObject().Raise; + } + + public void Update(List partyMembers) + { + if (!_config.Enabled) + { + return; + } + + Dictionary deadAndNotRaised = new Dictionary(); + float? limitBreakTime = null; + Dictionary raiseTimeMap = new Dictionary(); + + foreach (var member in partyMembers) + { + if (member.Character == null || member.ObjectId == 0) + { + member.RaiseTime = null; + continue; + } + + if (member.HP > 0) + { + member.RaiseTime = null; + } + + if (member.Character is not IBattleChara battleChara) + { + continue; + } + + // check raise casts + if (Utils.IsActorCasting(battleChara)) + { + var remaining = Math.Max(0, battleChara.TotalCastTime - battleChara.CurrentCastTime); + + // check limit break + if (IsRaiseLimitBreakAction(battleChara.CastActionId) && + (limitBreakTime.HasValue && limitBreakTime.Value > remaining)) + { + limitBreakTime = remaining; + } + // check regular raise + else if (IsRaiseAction(battleChara.CastActionId)) + { + if (raiseTimeMap.TryGetValue((uint)battleChara.CastTargetObjectId, out float raiseTime)) + { + if (raiseTime > remaining) + { + raiseTimeMap[(uint)battleChara.CastTargetObjectId] = remaining; + } + } + else + { + raiseTimeMap.Add((uint)battleChara.CastTargetObjectId, remaining); + } + } + } + + // check raise buff + if (member.HP <= 0) + { + bool hasBuff = false; + + var statusList = Utils.StatusListForBattleChara(battleChara); + foreach (var status in statusList) + { + if (status == null || (status.StatusId != 148 && status.StatusId != 1140)) + { + continue; + } + + // apply raise data based on buff + member.RaiseTime = status.RemainingTime; + hasBuff = true; + break; + } + + if (!hasBuff) + { + deadAndNotRaised.Add(member.ObjectId, member); + } + } + } + + // apply raise data based on casts + foreach (var memberId in deadAndNotRaised.Keys) + { + var member = deadAndNotRaised[memberId]; + + if (raiseTimeMap.TryGetValue(memberId, out float raiseTime)) + { + if (limitBreakTime.HasValue && limitBreakTime.Value < raiseTime) + { + member.RaiseTime = limitBreakTime; + } + else + { + member.RaiseTime = raiseTime; + } + } + else + { + member.RaiseTime = limitBreakTime; // its fine if this is null here + } + } + } + + #region raise ids + private static bool IsRaiseLimitBreakAction(uint actionId) + { + return LimitBreakIds.Contains(actionId); + } + + private static bool IsRaiseAction(uint actionId) + { + return RaiseIds.Contains(actionId); + } + + + private static List RaiseIds = new List() + { + 173, // ACN, SMN, SCH + 125, // CNH, WHM + 3603, // AST + 18317, // BLU + 22345, // Lost Sacrifice, Bozja + 20730, // Lost Arise, Bozja + 12996, // Raise L, Eureka + 24287 // SGE + }; + + private static List LimitBreakIds = new List() + { + 208, // WHM + 4247, // SCH + 4248, // AST + 24859 // SGE + }; + #endregion + } +} diff --git a/Interface/Party/PartyManager.cs b/Interface/Party/PartyManager.cs new file mode 100644 index 0000000..9fb5370 --- /dev/null +++ b/Interface/Party/PartyManager.cs @@ -0,0 +1,805 @@ +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Memory; +using HSUI.Config; +using HSUI.Helpers; +using FFXIVClientStructs.FFXIV.Client.Game.Group; +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.FFXIV.Client.UI.Info; +using Lumina.Excel.Sheets; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.UI.Arrays; +using FFXIVClientStructs.FFXIV.Component.GUI; +using static FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager; +using DalamudPartyMember = Dalamud.Game.ClientState.Party.IPartyMember; +using StructsPartyMember = FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember; + +namespace HSUI.Interface.Party +{ + public delegate void PartyMembersChangedEventHandler(PartyManager sender); + + public unsafe class PartyManager : IDisposable + { + #region Singleton + public static PartyManager Instance { get; private set; } = null!; + private PartyFramesConfig _config = null!; + private PartyFramesIconsConfig _iconsConfig = null!; + + private PartyManager() + { + _readyCheckHelper = new PartyReadyCheckHelper(); + _raiseTracker = new PartyFramesRaiseTracker(); + _invulnTracker = new PartyFramesInvulnTracker(); + _cleanseTracker = new PartyFramesCleanseTracker(); + + ConfigurationManager.Instance.ResetEvent += OnConfigReset; + + OnConfigReset(ConfigurationManager.Instance); + UpdatePreview(); + + // find offline string for active language + if (Plugin.DataManager.GetExcelSheet().TryGetRow(9836, out Addon row)) + { + _offlineString = row.Text.ToString(); + } + } + + public static void Initialize() + { + Instance = new PartyManager(); + } + + ~PartyManager() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + + _readyCheckHelper.Dispose(); + _raiseTracker.Dispose(); + _invulnTracker.Dispose(); + _cleanseTracker.Dispose(); + + _config.ValueChangeEvent -= OnConfigPropertyChanged; + + Instance = null!; + } + + private void OnConfigReset(ConfigurationManager sender) + { + if (_config != null) + { + _config.ValueChangeEvent -= OnConfigPropertyChanged; + } + + _config = sender.GetConfigObject(); + _config.ValueChangeEvent += OnConfigPropertyChanged; + + _iconsConfig = ConfigurationManager.Instance.GetConfigObject(); + } + + #endregion Singleton + + public AddonPartyList* PartyListAddon { get; private set; } = null; + public IntPtr HudAgent { get; private set; } = IntPtr.Zero; + + private RaptureAtkModule* _raptureAtkModule = null; + + private const int PartyListInfoOffset = 0x0D40; + private const int PartyListMemberRawInfoSize = 0x28; + + private const int PartyMembersInfoIndex = 12; // TODO: Should be reworked to use PartyMemberListStringArray.Instance() + + private List _groupMembers = new List(); + public IReadOnlyCollection GroupMembers => _groupMembers.AsReadOnly(); + + private List _sortedGroupMembers = new List(); + public IReadOnlyCollection SortedGroupMembers => _sortedGroupMembers.AsReadOnly(); + + public uint MemberCount => (uint)_groupMembers.Count; + + private string? _partyTitle = null; + public string PartyTitle => _partyTitle ?? ""; + + private int _groupMemberCount => GroupManager.Instance()->MainGroup.MemberCount; + private int _realMemberCount => PartyListAddon != null ? PartyListAddon->MemberCount : Plugin.PartyList.Length; + private int _realMemberAndChocoboCount => PartyListAddon != null ? PartyListAddon->MemberCount + Math.Max(1, (int)PartyListAddon->ChocoboCount) : Plugin.PartyList.Length; + + private Dictionary _prevDataMap = new(); + + private bool _wasRealGroup = false; + private bool _wasCrossWorld = false; + + private InfoProxyCrossRealm* _crossRealmInfo => InfoProxyCrossRealm.Instance(); + private Group _mainGroup => GroupManager.Instance()->MainGroup; + + private string _offlineString = "offline"; + + private PartyReadyCheckHelper _readyCheckHelper; + private PartyFramesRaiseTracker _raiseTracker; + private PartyFramesInvulnTracker _invulnTracker; + private PartyFramesCleanseTracker _cleanseTracker; + + public event PartyMembersChangedEventHandler? MembersChangedEvent; + + public bool Previewing => _config.Preview; + + public bool IsSoloParty() + { + if (!_config.ShowWhenSolo) { return false; } + + return _groupMembers.Count <= 1 || + (_groupMembers.Count == 2 && _config.ShowChocobo && + _groupMembers[1].Character is IBattleNpc npc && npc.BattleNpcKind == BattleNpcSubKind.Chocobo); + } + + public void Update() + { + // find party list hud agent + PartyListAddon = (AddonPartyList*)Plugin.GameGui.GetAddonByName("_PartyList", 1).Address; + HudAgent = Plugin.GameGui.FindAgentInterface(PartyListAddon); + + if (PartyListAddon == null || HudAgent == IntPtr.Zero) + { + if (_groupMembers.Count > 0) + { + _groupMembers.Clear(); + MembersChangedEvent?.Invoke(this); + } + + return; + } + + _raptureAtkModule = RaptureAtkModule.Instance(); + + // no need to update on preview mode + if (_config.Preview) + { + return; + } + + InternalUpdate(); + } + + private void InternalUpdate() + { + IPlayerCharacter? player = Plugin.ObjectTable.LocalPlayer; + if (player is null || player is not IPlayerCharacter) + { + return; + } + + bool isCrossWorld = IsCrossWorldParty(); + + // ready check update + if (_iconsConfig.ReadyCheckStatus.Enabled) + { + _readyCheckHelper.Update(_iconsConfig.ReadyCheckStatus.Duration); + } + + try + { + // title + _partyTitle = GetPartyListTitle(); + + // solo + if (_realMemberCount <= 1 && PartyListAddon->TrustCount == 0) + { + if (_config.ShowWhenSolo) + { + UpdateSoloParty(player); + } + else if (_groupMembers.Count > 0) + { + _groupMembers.Clear(); + MembersChangedEvent?.Invoke(this); + } + + _wasRealGroup = false; + } + else + { + // player maps + Dictionary dataMap = GetMembersDataMap(player, isCrossWorld); + bool partyChanged = _prevDataMap.Count != dataMap.Count; + + if (!partyChanged) + { + foreach (string key in dataMap.Keys) + { + InternalMemberData newData = dataMap[key]; + if (!_prevDataMap.TryGetValue(key, out InternalMemberData? oldData) || + oldData == null || + newData.Order != oldData.Order) + { + partyChanged = true; + break; + } + } + } + + _prevDataMap = dataMap; + + // trust + if (PartyListAddon->TrustCount > 0) + { + UpdateTrustParty(player, dataMap, partyChanged); + } + // cross world party/alliance + else if (isCrossWorld) + { + UpdateCrossWorldParty(player, dataMap, partyChanged); + } + // regular party + else + { + UpdateRegularParty(player, dataMap, partyChanged); + } + + _wasRealGroup = true; + } + + UpdateTrackers(); + } + catch (Exception e) + { + Plugin.Logger.Warning(e.Message); + } + + _wasCrossWorld = isCrossWorld; + } + + private Dictionary GetMembersDataMap(IPlayerCharacter player, bool isCrossWorld) + { + Dictionary dataMap = new Dictionary(); + + if (_raptureAtkModule == null || _raptureAtkModule->AtkModule.AtkArrayDataHolder.StringArrayCount <= PartyMembersInfoIndex) + { + return dataMap; + } + + // raw info + int allianceNum = FindAlliance(player); + int count = isCrossWorld ? _crossRealmInfo->CrossRealmGroups[allianceNum].GroupMemberCount : _realMemberCount + PartyListAddon->TrustCount; + for (int i = 0; i < count; i++) + { + InternalMemberData data = new InternalMemberData(); + data.Index = i; + + if (!isCrossWorld) + { + PartyListMemberRawInfo* info = (PartyListMemberRawInfo*)(HudAgent + (PartyListInfoOffset + PartyListMemberRawInfoSize * i)); + data.ObjectId = info->ObjectId; + data.ContentId = info->ContentId; + data.Name = info->Name; + data.Order = info->Order; + } + else + { + CrossRealmMember member = _crossRealmInfo->CrossRealmGroups[allianceNum].GroupMembers[i]; + data.ObjectId = member.EntityId; + data.ContentId = (long)member.ContentId; + data.Name = member.NameString; + data.Order = i; + } + + if (!dataMap.ContainsKey(data.Name)) + { + dataMap.Add(data.Name, data); + } + } + + // status string + var stringArrayData = _raptureAtkModule->AtkModule.AtkArrayDataHolder.StringArrays[PartyMembersInfoIndex]; + for (int i = 0; i < count; i++) + { + int index = i * 5; + if (stringArrayData->AtkArrayData.Size <= index + 3 || + stringArrayData->StringArray[index] == null || + stringArrayData->StringArray[index + 3] == null) { break; } + + IntPtr ptr = new IntPtr(stringArrayData->StringArray[index]); + string name = MemoryHelper.ReadSeStringNullTerminated(ptr).ToString(); + + ptr = new IntPtr(stringArrayData->StringArray[index + 3]); + + string a = MemoryHelper.ReadSeStringNullTerminated(ptr).ToString(); + + + if (dataMap.TryGetValue(name, out InternalMemberData? data) && data != null) + { + data.Status = MemoryHelper.ReadSeStringNullTerminated(ptr).ToString(); + } + } + + return dataMap; + } + + private bool IsCrossWorldParty() + { + return _crossRealmInfo->IsCrossRealm && _crossRealmInfo->GroupCount > 0 && _mainGroup.MemberCount == 0; + } + + private ReadyCheckStatus GetReadyCheckStatus(ulong contentId) + { + return _readyCheckHelper.GetStatusForContentId(contentId); + } + + private void UpdateTrustParty(IPlayerCharacter player, Dictionary dataMap, bool forced) + { + bool softUpdate = true; + + if (_groupMembers.Count != dataMap.Count || forced) + { + _groupMembers.Clear(); + softUpdate = false; + } + + if (softUpdate) + { + foreach (IPartyFramesMember member in _groupMembers) + { + if (member.ObjectId == player.GameObjectId) + { + member.Update(EnmityForIndex(member.Index), PartyMemberStatus.None, ReadyCheckStatus.None, true, player.ClassJob.RowId); + } + else + { + member.Update(EnmityForTrustMemberIndex(member.Index - 1), PartyMemberStatus.None, ReadyCheckStatus.None, false, 0); + } + } + } + else + { + string[] keys = dataMap.Keys.ToArray(); + for (int i = 0; i < keys.Length; i++) + { + InternalMemberData data = dataMap[keys[i]]; + if (keys[i] == player.Name.ToString()) + { + PartyFramesMember playerMember = new PartyFramesMember(player, data.Index, data.Order, EnmityForIndex(i), PartyMemberStatus.None, ReadyCheckStatus.None, true); + _groupMembers.Add(playerMember); + } + else + { + ICharacter? trustChara = Utils.GetGameObjectByName(keys[i]) as ICharacter; + if (trustChara != null) + { + _groupMembers.Add(new PartyFramesMember(trustChara, data.Index, data.Order, EnmityForTrustMemberIndex(i), PartyMemberStatus.None, ReadyCheckStatus.None, false)); + } + } + } + } + + if (!softUpdate) + { + SortGroupMembers(player); + MembersChangedEvent?.Invoke(this); + } + } + + private void UpdateSoloParty(IPlayerCharacter player) + { + ICharacter? chocobo = null; + if (_config.ShowChocobo) + { + var gameObject = Utils.GetBattleChocobo(player); + if (gameObject != null && gameObject is ICharacter) + { + chocobo = (ICharacter)gameObject; + } + } + + bool needsUpdate = + _groupMembers.Count == 0 || + (_groupMembers.Count != 2 && _config.ShowChocobo && chocobo != null) || + (_groupMembers.Count > 1 && !_config.ShowChocobo) || + (_groupMembers.Count > 1 && chocobo == null) || + (_groupMembers.Count == 2 && _config.ShowChocobo && _groupMembers[1].ObjectId != chocobo?.EntityId); + + EnmityLevel playerEnmity = PartyListAddon->EnmityLeaderIndex == 0 ? EnmityLevel.Leader : EnmityLevel.Last; + + // for some reason chocobos never get a proper enmity value even though they have aggro + // if the player enmity is set to first, but the "leader index" is invalid + // we can pretty much deduce that the chocobo is the one with aggro + // this might fail on some cases when there are other players not in party hitting the same thing + // but the edge case is so minor we should be fine + EnmityLevel chocoboEnmity = PartyListAddon->EnmityLeaderIndex == -1 && PartyListAddon->PartyMembers[0].EmnityByte == 1 ? EnmityLevel.Leader : EnmityLevel.Last; + + if (needsUpdate) + { + _groupMembers.Clear(); + + _groupMembers.Add(new PartyFramesMember(player, 0, 0, playerEnmity, PartyMemberStatus.None, ReadyCheckStatus.None, true)); + + if (chocobo != null) + { + _groupMembers.Add(new PartyFramesMember(chocobo, 1, 1, chocoboEnmity, PartyMemberStatus.None, ReadyCheckStatus.None, false)); + } + + SortGroupMembers(player); + MembersChangedEvent?.Invoke(this); + } + else + { + for (int i = 0; i < _groupMembers.Count; i++) + { + _groupMembers[i].Update(i == 0 ? playerEnmity : chocoboEnmity, PartyMemberStatus.None, ReadyCheckStatus.None, i == 0, i == 0 ? player.ClassJob.RowId : 0); + } + } + } + + private void UpdateCrossWorldParty(IPlayerCharacter player, Dictionary dataMap, bool forced) + { + bool softUpdate = true; + int allianceNum = FindAlliance(player); + + int count = _crossRealmInfo->CrossRealmGroups[allianceNum].GroupMemberCount; + if (!_wasCrossWorld || count != _groupMembers.Count || forced) + { + _groupMembers.Clear(); + softUpdate = false; + } + + // create new members array with cross world data + for (int i = 0; i < count; i++) + { + CrossRealmMember member = _crossRealmInfo->CrossRealmGroups[allianceNum].GroupMembers[i]; + string memberName = member.NameString; + + if (!dataMap.TryGetValue(memberName, out InternalMemberData? data) || data == null) + { + continue; + } + + bool isPlayer = member.EntityId == player.EntityId; + bool isLeader = member.IsPartyLeader; + PartyMemberStatus status = data.Status != null ? StatusForCrossWorldMember(data.Status) : PartyMemberStatus.None; + ReadyCheckStatus readyCheckStatus = GetReadyCheckStatus(member.ContentId); + + if (softUpdate) + { + IPartyFramesMember groupMember = _groupMembers.ElementAt(i); + groupMember.Update(EnmityLevel.Last, status, readyCheckStatus, isLeader, member.ClassJobId); + } + else + { + PartyFramesMember partyMember = isPlayer ? + new PartyFramesMember(player, i, data.Order, EnmityLevel.Last, status, readyCheckStatus, isLeader) : + new PartyFramesMember(member, i, data.Order, status, readyCheckStatus, isLeader); + _groupMembers.Add(partyMember); + } + } + + if (!softUpdate) + { + SortGroupMembers(player); + MembersChangedEvent?.Invoke(this); + } + } + + private int FindAlliance(IPlayerCharacter player) + { + for (int i = 0; i < _crossRealmInfo->CrossRealmGroups.Length; i++) + { + for (int j = 0; j < _crossRealmInfo->CrossRealmGroups[i].GroupMemberCount; j++) + { + CrossRealmMember member = _crossRealmInfo->CrossRealmGroups[i].GroupMembers[j]; + if (member.EntityId == player.EntityId) + { + return i; + } + } + } + return 0; + } + + private void UpdateRegularParty(IPlayerCharacter player, Dictionary dataMap, bool forced) + { + bool softUpdate = true; + + if (!_wasRealGroup || _realMemberCount != _groupMembers.Count || forced) + { + _groupMembers.Clear(); + softUpdate = false; + } + + string[] keys = dataMap.Keys.ToArray(); + for (int i = 0; i < keys.Length; i++) + { + if (!dataMap.TryGetValue(keys[i], out InternalMemberData? data) || data == null) + { + continue; + } + + bool isPlayer = data.ObjectId == player.GameObjectId; + bool isLeader = IsPartyLeader(data.Order); + EnmityLevel enmity = EnmityForIndex(data.Index); + PartyMemberStatus status = data.Status != null ? StatusForMember(data.Status, data.Name) : PartyMemberStatus.None; + ReadyCheckStatus readyCheckStatus = GetReadyCheckStatus((ulong)data.ContentId); + + if (softUpdate) + { + IPartyFramesMember groupMember = _groupMembers.ElementAt(i); + groupMember.Update(enmity, status, readyCheckStatus, isLeader); + } + else + { + PartyFramesMember partyMember; + var member = GetDalamudPartyMember(data.Name); + if (member.HasValue && member.Value.Item1 is DalamudPartyMember dalamudPartyMember) + { + partyMember = new PartyFramesMember(dalamudPartyMember, i, data.Order, enmity, status, readyCheckStatus, isLeader); + } + else + { + partyMember = new PartyFramesMember(data.ObjectId, i, data.Order, enmity, status, readyCheckStatus, isLeader); + } + _groupMembers.Add(partyMember); + } + } + + // player's chocobo (always last) + if (_config.ShowChocobo) + { + IGameObject? companion = Utils.GetBattleChocobo(player); + + if (softUpdate && _groupMembers.FirstOrDefault(o => o.IsChocobo) is PartyFramesMember chocoboMember) + { + if (companion is ICharacter) + { + chocoboMember.Update(EnmityLevel.Last, PartyMemberStatus.None, ReadyCheckStatus.None, false); + } + else + { + _groupMembers.Remove(chocoboMember); + } + } + else if (companion is ICharacter companionCharacter) + { + _groupMembers.Add(new PartyFramesMember(companionCharacter, _groupMemberCount, 10, EnmityLevel.Last, PartyMemberStatus.None, ReadyCheckStatus.None, false, true)); + } + } + + if (!softUpdate) + { + SortGroupMembers(player); + MembersChangedEvent?.Invoke(this); + } + } + + + private void SortGroupMembers(IPlayerCharacter? player = null) + { + _sortedGroupMembers.Clear(); + _sortedGroupMembers.AddRange(_groupMembers); + + _sortedGroupMembers.Sort((a, b) => + { + if (a.Order == b.Order) + { + if (a.ObjectId == player?.GameObjectId) + { + return 1; + } + else if (b.ObjectId == player?.GameObjectId) + { + return -1; + } + + return a.Name.CompareTo(b.Name); + } + + if (a.Order < b.Order) + { + return -1; + } + + return 1; + }); + } + + private (DalamudPartyMember?, int)? GetDalamudPartyMember(string name) + { + for (int i = 0; i < Plugin.PartyList.Length; i++) + { + DalamudPartyMember? member = Plugin.PartyList[i]; + if (member != null && member.Name.ToString() == name) + { + return (member, i); + } + } + + return null; + } + + private void UpdateTrackers() + { + _raiseTracker.Update(_groupMembers); + _invulnTracker.Update(_groupMembers); + _cleanseTracker.Update(_groupMembers); + } + + #region utils + private bool IsPartyLeader(int index) + { + if (PartyListAddon == null) + { + return false; + } + + // we use the icon Y coordinate in the party list to know the index (lmao) + uint partyLeadIndex = (uint)PartyListAddon->LeaderMarkResNode->ChildNode->Y / 40; + return index == partyLeadIndex; + } + + private PartyMemberStatus StatusForCrossWorldMember(string statusStr) + { + // offline status + if (statusStr.Contains(_offlineString, StringComparison.InvariantCultureIgnoreCase)) + { + return PartyMemberStatus.Offline; + } + + return PartyMemberStatus.None; + } + + + private PartyMemberStatus StatusForMember(string statusStr, string name) + { + // offline status + if (statusStr.Contains(_offlineString, StringComparison.InvariantCultureIgnoreCase)) + { + return PartyMemberStatus.Offline; + } + + // viewing cutscene status + for (int i = 0; i < _mainGroup.MemberCount; i++) + { + if (_mainGroup.PartyMembers[i].NameString == name) + { + if ((_mainGroup.PartyMembers[i].Flags & 0x10) != 0) + { + return PartyMemberStatus.ViewingCutscene; + } + break; + } + } + + return PartyMemberStatus.None; + } + + private EnmityLevel EnmityForIndex(int index) + { + if (PartyListAddon == null || index < 0 || index > 7) + { + return EnmityLevel.Last; + } + + EnmityLevel enmityLevel = (EnmityLevel)PartyListAddon->PartyMembers[index].EmnityByte; + if (enmityLevel == EnmityLevel.Leader && PartyListAddon->EnmityLeaderIndex != index) + { + enmityLevel = EnmityLevel.Last; + } + + return enmityLevel; + } + + private EnmityLevel EnmityForTrustMemberIndex(int index) + { + if (PartyListAddon == null || index < 0 || index > 6) + { + return EnmityLevel.Last; + } + + return (EnmityLevel)PartyListAddon->TrustMembers[index].EmnityByte; + } + + private static unsafe string? GetPartyListTitle() + { + AgentModule* agentModule = AgentModule.Instance(); + if (agentModule == null) { return ""; } + + AgentHUD* agentHUD = agentModule->GetAgentHUD(); + if (agentHUD == null) { return ""; } + + Lumina.Excel.ExcelSheet sheet = Plugin.DataManager.GetExcelSheet(); + if (sheet.TryGetRow(agentHUD->PartyTitleAddonId, out Addon row)) + { + return row.Text.ToString(); + } + + return null; + } + #endregion + + #region events + private void OnConfigPropertyChanged(object sender, OnChangeBaseArgs args) + { + if (args.PropertyName == "Preview") + { + UpdatePreview(); + } + } + + public void UpdatePreview() + { + _iconsConfig.Sign.Preview = _config.Preview; + + if (!_config.Preview) + { + _groupMembers.Clear(); + MembersChangedEvent?.Invoke(this); + return; + } + + // fill list with fake members for UI testing + _groupMembers.Clear(); + + if (_config.Preview) + { + int count = FakePartyFramesMember.RNG.Next(4, 9); + for (int i = 0; i < count; i++) + { + _groupMembers.Add(new FakePartyFramesMember(i)); + } + } + + SortGroupMembers(); + MembersChangedEvent?.Invoke(this); + } + #endregion + } + + internal class InternalMemberData + { + internal uint ObjectId = 0; + internal long ContentId = 0; + internal string Name = ""; + internal uint JobId = 0; + internal int Order = 0; + internal int Index = 0; + internal string? Status = null; + + public InternalMemberData() + { + } + } + + [StructLayout(LayoutKind.Explicit, Size = 28)] + public unsafe struct PartyListMemberRawInfo + { + [FieldOffset(0x00)] public byte* NamePtr; + [FieldOffset(0x08)] public long ContentId; + [FieldOffset(0x10)] public uint ObjectId; + + // some kind of type + // 1 = player + // 2 = party member? + // 3 = unknown + // 4 = chocobo + // 5 = summon? + [FieldOffset(0x14)] public byte Type; + + [FieldOffset(0x18)] public byte Order; + + public string Name => Marshal.PtrToStringUTF8(new IntPtr(NamePtr)) ?? ""; + } +} diff --git a/Interface/Party/PartyOrderHelper.cs b/Interface/Party/PartyOrderHelper.cs new file mode 100644 index 0000000..6d619e1 --- /dev/null +++ b/Interface/Party/PartyOrderHelper.cs @@ -0,0 +1,171 @@ +using Dalamud.Game.ClientState.Objects.SubKinds; +using HSUI.Helpers; +using FFXIVClientStructs.FFXIV.Client.System.Framework; +using FFXIVClientStructs.FFXIV.Client.UI.Misc; +using System.Collections.Generic; + +namespace HSUI.Interface.Party +{ + public static class PartyOrderHelper + { + private enum PartySortingSetting + { + Tank_Healer_DPS = 0, + Tank_DPS_Healer = 1, + Healer_Tank_DPS = 2, + Healer_DPS_Tank = 3, + DPS_Tank_Healer = 4, + DPS_Healer_Tank = 5, + Count = 6 + } + + private class PartyRoles + { + internal int Tank; + internal int Healer; + internal int DPS; + internal int Other; + + public PartyRoles() + { + Tank = 0; + Healer = 0; + DPS = 0; + Other = 0; + } + + public PartyRoles(int tank, int healer, int dps, int other) + { + Tank = tank; + Healer = healer; + DPS = dps; + Other = other; + } + } + + // calcualates the position for the player if they select the + // option to always appear as the first of their current role + // in the party frames + public static int? GetRoleFirstOrder(List members) + { + IPlayerCharacter? player = Plugin.ObjectTable.LocalPlayer; + if (player == null) { return null; } + + JobRoles role = JobsHelper.RoleForJob(player.ClassJob.RowId); + + PartySortingSetting? setting = GetPartySortingSetting(role); + if (!setting.HasValue) { return null; } + + PartyRoles rolesCount = GetPartyCountByRole(members); + PartyRoles roleWeights = GetRoleWeights(role, setting.Value); + + return rolesCount.Tank * roleWeights.Tank + + rolesCount.Healer * roleWeights.Healer + + rolesCount.DPS * roleWeights.DPS + + rolesCount.Other * roleWeights.Other; + } + + private static unsafe PartySortingSetting? GetPartySortingSetting(JobRoles role) + { + ConfigModule* config = ConfigModule.Instance(); + if (config == null) { return null; } + + ConfigOption option; + switch (role) + { + case JobRoles.Tank: option = ConfigOption.PartyListSortTypeTank; break; + + case JobRoles.Healer: option = ConfigOption.PartyListSortTypeHealer; break; + + case JobRoles.DPSMelee: + case JobRoles.DPSRanged: + case JobRoles.DPSCaster: option = ConfigOption.PartyListSortTypeDps; break; + + default: option = ConfigOption.PartyListSortTypeOther; break; + } + + Framework* framework = Framework.Instance(); + if (framework == null || framework->SystemConfig.SystemConfigBase.UiConfig.ConfigCount <= (int)option) { + return PartySortingSetting.Tank_Healer_DPS; + } + + uint value = framework->SystemConfig.SystemConfigBase.UiConfig.ConfigEntry[(int)option].Value.UInt; + if (value < 0 || value > (int)PartySortingSetting.Count) { return null; } + + return (PartySortingSetting)value; + } + + private static unsafe PartyRoles GetPartyCountByRole(List members) + { + PartyRoles rolesCount = new PartyRoles(); + + IPlayerCharacter? player = Plugin.ObjectTable.LocalPlayer; + if (player == null) { return rolesCount; } + + foreach (IPartyFramesMember member in members) + { + if (member.ObjectId == player.GameObjectId) { continue; } + + JobRoles role = JobsHelper.RoleForJob(member.JobId); + switch (role) + { + case JobRoles.Tank: rolesCount.Tank++; break; + + case JobRoles.Healer: rolesCount.Healer++; break; + + case JobRoles.DPSMelee: + case JobRoles.DPSRanged: + case JobRoles.DPSCaster: rolesCount.DPS++; break; + + default: rolesCount.Other++; break; + } + } + + return rolesCount; + } + + private static unsafe PartyRoles GetRoleWeights(JobRoles role, PartySortingSetting setting) + { + if (role == JobRoles.Crafter || role == JobRoles.Gatherer || role == JobRoles.Unknown) + { + return new PartyRoles(1, 1, 1, 0); + } + + JobRoles mapRole = role == JobRoles.DPSRanged || role == JobRoles.DPSCaster ? JobRoles.DPSMelee : role; + return RoleWeights[mapRole][setting]; + } + + private static Dictionary> RoleWeights = new Dictionary>() + { + [JobRoles.Tank] = new Dictionary() + { + [PartySortingSetting.Tank_Healer_DPS] = new PartyRoles(), + [PartySortingSetting.Tank_DPS_Healer] = new PartyRoles(), + [PartySortingSetting.Healer_Tank_DPS] = new PartyRoles(0, 1, 0, 0), + [PartySortingSetting.Healer_DPS_Tank] = new PartyRoles(0, 1, 1, 0), + [PartySortingSetting.DPS_Tank_Healer] = new PartyRoles(0, 0, 1, 0), + [PartySortingSetting.DPS_Healer_Tank] = new PartyRoles(0, 1, 1, 0) + }, + + [JobRoles.Healer] = new Dictionary() + { + [PartySortingSetting.Tank_Healer_DPS] = new PartyRoles(1, 0, 0, 0), + [PartySortingSetting.Tank_DPS_Healer] = new PartyRoles(1, 0, 1, 0), + [PartySortingSetting.Healer_Tank_DPS] = new PartyRoles(), + [PartySortingSetting.Healer_DPS_Tank] = new PartyRoles(), + [PartySortingSetting.DPS_Tank_Healer] = new PartyRoles(1, 0, 1, 0), + [PartySortingSetting.DPS_Healer_Tank] = new PartyRoles(0, 0, 1, 0) + }, + + [JobRoles.DPSMelee] = new Dictionary() + { + [PartySortingSetting.Tank_Healer_DPS] = new PartyRoles(1, 1, 0, 0), + [PartySortingSetting.Tank_DPS_Healer] = new PartyRoles(1, 0, 0, 0), + [PartySortingSetting.Healer_Tank_DPS] = new PartyRoles(1, 1, 0, 0), + [PartySortingSetting.Healer_DPS_Tank] = new PartyRoles(0, 1, 0, 0), + [PartySortingSetting.DPS_Tank_Healer] = new PartyRoles(), + [PartySortingSetting.DPS_Healer_Tank] = new PartyRoles() + } + }; + } +} diff --git a/Interface/Party/PartyReadyCheckHelper.cs b/Interface/Party/PartyReadyCheckHelper.cs new file mode 100644 index 0000000..3ed4f4b --- /dev/null +++ b/Interface/Party/PartyReadyCheckHelper.cs @@ -0,0 +1,156 @@ +using Dalamud.Hooking; +using Dalamud.Logging; +using Dalamud.Memory; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.FFXIV.Client.UI.Info; +using Dalamud.Bindings.ImGui; +using System; + +namespace HSUI.Interface.Party +{ + public enum ReadyCheckStatus + { + Ready = 0, + NotReady = 1, + None = 2 + } + + public class PartyReadyCheckHelper : IDisposable + { + private delegate void ReadyCheckDelegate(IntPtr ptr); + private Hook? _onReadyCheckStartHook; + private Hook? _onReadyCheckEndHook; + + private delegate void ActorControlDelegate(uint entityId, uint type, uint buffID, uint direct, uint actionId, uint sourceId, uint arg7, uint arg8, uint arg9, uint arg10, ulong targetId, byte arg12); + private Hook? _actorControlHook; + + private bool _readyCheckOngoing = false; + private double _lastReadyCheckEndTime = -1; + + + public unsafe PartyReadyCheckHelper() + { + try + { + _onReadyCheckStartHook = Plugin.GameInteropProvider.HookFromAddress( + AgentReadyCheck.MemberFunctionPointers.InitiateReadyCheck, + OnReadyCheckStart + ); + _onReadyCheckStartHook?.Enable(); + + _onReadyCheckEndHook = Plugin.GameInteropProvider.HookFromAddress( + AgentReadyCheck.MemberFunctionPointers.EndReadyCheck, + OnReadycheckEnd + ); + _onReadyCheckEndHook?.Enable(); + + _actorControlHook = Plugin.GameInteropProvider.HookFromSignature( + "E8 ?? ?? ?? ?? 0F B7 0B 83 E9 64", + OnActorControl + ); + _actorControlHook?.Enable(); + } + catch (Exception e) + { + Plugin.Logger.Error("Error initiating ready check sigs!!!\n" + e.Message); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + + _onReadyCheckStartHook?.Disable(); + _onReadyCheckStartHook?.Dispose(); + + _onReadyCheckEndHook?.Disable(); + _onReadyCheckEndHook?.Dispose(); + + _actorControlHook?.Disable(); + _actorControlHook?.Dispose(); + } + + private unsafe void OnReadyCheckStart(AgentReadyCheck *ptr) + { + _onReadyCheckStartHook?.Original(ptr); + _readyCheckOngoing = true; + _lastReadyCheckEndTime = -1; + } + + private unsafe void OnReadycheckEnd(AgentReadyCheck *ptr) + { + _onReadyCheckEndHook?.Original(ptr); + _lastReadyCheckEndTime = ImGui.GetTime(); + } + + private void OnActorControl(uint entityId, uint type, uint buffID, uint direct, uint actionId, uint sourceId, uint arg7, uint arg8, uint arg9, uint arg10, ulong targetId, byte arg12) + { + _actorControlHook?.Original(entityId, type, buffID, direct, actionId, sourceId, arg7, arg8, arg9, arg10, targetId, arg12); + + // I'm not exactly sure what id == 503 means, but its always triggered when the fight starts + // which is all I care about + if (type == 503) + { + _readyCheckOngoing = false; + } + } + + public void Update(double maxDuration) + { + if (_readyCheckOngoing && + _lastReadyCheckEndTime != -1 && + ImGui.GetTime() - _lastReadyCheckEndTime >= maxDuration) + { + _readyCheckOngoing = false; + } + } + + public unsafe ReadyCheckStatus GetStatusForContentId(ulong contentId) + { + if (!_readyCheckOngoing) + { + return ReadyCheckStatus.None; + } + + try + { + for (int i = 0; i < 8; i++) + { + ReadyCheckEntry entry = AgentReadyCheck.Instance()->ReadyCheckEntries[i]; + if (entry.ContentId == contentId) + { + return ParseStatus(entry); + } + } + } + catch { } + + return ReadyCheckStatus.None; + } + + private ReadyCheckStatus ParseStatus(ReadyCheckEntry entry) + { + if (entry.Status == FFXIVClientStructs.FFXIV.Client.UI.Agent.ReadyCheckStatus.Ready) + { + return ReadyCheckStatus.Ready; + } + else if (entry.Status == FFXIVClientStructs.FFXIV.Client.UI.Agent.ReadyCheckStatus.NotReady || + entry.Status == FFXIVClientStructs.FFXIV.Client.UI.Agent.ReadyCheckStatus.MemberNotPresent) + { + return ReadyCheckStatus.NotReady; + } + + return ReadyCheckStatus.None; + } + } +} + diff --git a/Interface/PartyCooldowns/PartyCooldown.cs b/Interface/PartyCooldowns/PartyCooldown.cs new file mode 100644 index 0000000..b299388 --- /dev/null +++ b/Interface/PartyCooldowns/PartyCooldown.cs @@ -0,0 +1,220 @@ +using HSUI.Helpers; +using HSUI.Interface.Party; +using Dalamud.Bindings.ImGui; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; + +namespace HSUI.Interface.PartyCooldowns +{ + public enum PartyCooldownEnabled + { + PartyCooldownsAndPartyFrames = 0, + PartyCooldowns = 1, + PartyFrames = 2, + Disabled = 3 + } + + public class PartyCooldown + { + public readonly PartyCooldownData Data; + public readonly uint SourceId; + public readonly uint MemberLevel; + public readonly IPartyFramesMember? Member; + + public double LastTimeUsed = 0; + public double OverridenCooldownStartTime = -1; + public bool IgnoreNextUse = false; + + public PartyCooldown(PartyCooldownData data, uint sourceID, uint level, IPartyFramesMember? member) + { + Data = data; + SourceId = sourceID; + MemberLevel = level; + Member = member; + } + + public float EffectTimeRemaining() + { + int duration = GetEffectDuration(); + double timeSinceUse = ImGui.GetTime() - LastTimeUsed; + if (IgnoreNextUse || timeSinceUse > duration) + { + return 0; + } + + return duration - (float)timeSinceUse; + } + + public float CooldownTimeRemaining() + { + float cooldown = GetCooldown(); + double timeSinceUse = OverridenCooldownStartTime != -1 ? ImGui.GetTime() - OverridenCooldownStartTime : ImGui.GetTime() - LastTimeUsed; + + if (timeSinceUse > cooldown) + { + OverridenCooldownStartTime = -1; + LastTimeUsed = 0; + return 0; + } + + return cooldown - (float)timeSinceUse; + } + + public int GetCooldown() + { + // special case for troubadour, shield samba and tactician + if (Data.ActionId == 7405 || Data.ActionId == 16012 || Data.ActionId == 16889) + { + return MemberLevel < 88 ? Data.CooldownDuration : 90; + } + // special case for swiftcast + else if (Data.ActionId == 7561) + { + return MemberLevel < 94 ? Data.CooldownDuration : 40; + } + + return Data.CooldownDuration; + } + + public int GetEffectDuration() + { + // special case for reprisal, feint and addle + if (MemberLevel < 98) { return Data.EffectDuration; } + if (Data.ActionId != 7535 && Data.ActionId != 7549 && Data.ActionId != 7560) { return Data.EffectDuration; } + + return 15; + } + + public string TooltipText() + { + int duration = GetEffectDuration(); + string effectDuration = duration > 0 ? $"Duration: {duration}s \n" : ""; + return $"{effectDuration}Recast Time: {GetCooldown()}s"; + } + } + + public class PartyCooldownData : IEquatable + { + public PartyCooldownEnabled EnabledV2 = PartyCooldownEnabled.PartyCooldownsAndPartyFrames; + + public uint ActionId = 0; + public uint RequiredLevel = 0; + public uint DisabledAfterLevel = 0; + + public uint JobId = 0; // keep this for backwards compatibility + public List? JobIds = null; + + public JobRoles Role = JobRoles.Unknown; // keep this for backwards compatibility + public List? Roles = null; + + public HashSet ExcludedJobIds = new(); + + public int CooldownDuration = 0; + public int EffectDuration = 0; + + public int Priority = 0; + public int Column = 1; + + public List SharedActionIds = new(); + + [JsonIgnore] public uint IconId = 0; + [JsonIgnore] public string Name = ""; + [JsonIgnore] public string? OverriddenCooldownText = null; + [JsonIgnore] public string? OverriddenDurationText = null; + + public virtual bool IsUsableBy(uint jobId) + { + JobRoles roleForJob = JobsHelper.RoleForJob(jobId); + + if (Roles != null) + { + foreach (JobRoles role in Roles) + { + if (role == roleForJob) + { + return true; + } + } + + return false; + } + + if (Role != JobRoles.Unknown) + { + return Role == roleForJob; + } + + if (JobIds != null) + { + foreach (uint id in JobIds) + { + if (id == jobId) + { + return true; + } + } + } + + return JobId == jobId; + } + + public bool HasRole(JobRoles role) + { + if (Roles != null) + { + return Roles.Contains(role); + } + + if (Role != JobRoles.Unknown) + { + return Role == role; + } + + if (JobIds != null) + { + foreach (uint jobId in JobIds) + { + JobRoles roleForJob = JobsHelper.RoleForJob(jobId); + if (roleForJob == role) + { + return true; + } + } + + return false; + } + + return JobsHelper.RoleForJob(JobId) == role; + } + + public bool IsEnabledForPartyCooldowns() + { + return EnabledV2 == PartyCooldownEnabled.PartyCooldownsAndPartyFrames || + EnabledV2 == PartyCooldownEnabled.PartyCooldowns; + } + + public bool IsEnabledForPartyFrames() + { + return EnabledV2 == PartyCooldownEnabled.PartyCooldownsAndPartyFrames || + EnabledV2 == PartyCooldownEnabled.PartyFrames; + } + + public bool Equals(PartyCooldownData? other) + { + if (other == null) { return false; } + + return + ActionId == other.ActionId && + RequiredLevel == other.RequiredLevel && + DisabledAfterLevel == other.DisabledAfterLevel && + JobId == other.JobId && + (JobIds == null && other.JobIds == null || (JobIds != null && other.JobIds != null && JobIds.Equals(other.JobIds))) && + Role == other.Role && + (Roles == null && other.Roles == null || (Roles != null && other.Roles != null && Roles.Equals(other.Roles))) && + CooldownDuration == other.CooldownDuration && + EffectDuration == other.EffectDuration && + SharedActionIds == other.SharedActionIds; + } + } +} diff --git a/Interface/PartyCooldowns/PartyCooldownsConfig.cs b/Interface/PartyCooldowns/PartyCooldownsConfig.cs new file mode 100644 index 0000000..df1bafb --- /dev/null +++ b/Interface/PartyCooldowns/PartyCooldownsConfig.cs @@ -0,0 +1,884 @@ +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 System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using LuminaAction = Lumina.Excel.Sheets.Action; + +namespace HSUI.Interface.PartyCooldowns +{ + public enum PartyCooldownsGrowthDirection + { + Down = 0, + Up, + Right, + Left + } + + [Exportable(false)] + [Section("Party Cooldowns", true)] + [SubSection("General", 0)] + public class PartyCooldownsConfig : MovablePluginConfigObject + { + public new static PartyCooldownsConfig DefaultConfig() + { + var config = new PartyCooldownsConfig(); + config.Position = new Vector2(-ImGui.GetMainViewport().Size.X / 2 + 100, -ImGui.GetMainViewport().Size.Y / 2 + 100); + + return config; + } + + [Checkbox("Preview", isMonitored = true)] + [Order(4)] + public bool Preview = false; + + [Combo("Sections Growth Direction", "Down", "Up", "Right", "Left", spacing = true)] + [Order(20)] + public PartyCooldownsGrowthDirection GrowthDirection = PartyCooldownsGrowthDirection.Down; + + [DragInt2("Padding", min = -1000, max = 1000)] + [Order(15)] + public Vector2 Padding = new Vector2(0, -1); + + [Checkbox("Tooltips", spacing = true)] + [Order(16)] + public bool ShowTooltips = true; + + [Checkbox("Show Only in Duties", spacing = true, isMonitored = true)] + [Order(20)] + public bool ShowOnlyInDuties = true; + + [Checkbox("Show When Solo", isMonitored = true)] + [Order(21)] + public bool ShowWhenSolo = false; + + [NestedConfig("Visibility", 200)] + public VisibilityConfig VisibilityConfig = new VisibilityConfig(); + } + + [Disableable(false)] + [Exportable(false)] + [DisableParentSettings("Position", "Anchor", "HideWhenInactive", "FillColor", "Background", "FillDirection")] + [Section("Party Cooldowns", true)] + [SubSection("Cooldown Bar", 0)] + public class PartyCooldownsBarConfig : BarConfig + { + [Checkbox("Show Bar", spacing = true)] + [Order(70)] + public bool ShowBar = true; + + [ColorEdit4("Available Color")] + [Order(71, collapseWith = nameof(ShowBar))] + public PluginConfigColor AvailableColor = new PluginConfigColor(new Vector4(0f / 255f, 150f / 255f, 0f / 255f, 100f / 100f)); + + [ColorEdit4("Available Background Color")] + [Order(72, collapseWith = nameof(ShowBar))] + public PluginConfigColor AvailableBackgroundColor = new PluginConfigColor(new Vector4(0f / 255f, 150f / 255f, 0f / 255f, 25f / 100f)); + + [ColorEdit4("Recharging Color")] + [Order(73, collapseWith = nameof(ShowBar))] + public PluginConfigColor RechargingColor = new PluginConfigColor(new Vector4(150f / 255f, 0f / 255f, 0f / 255f, 100f / 100f)); + + [ColorEdit4("Recharging Background Color")] + [Order(74, collapseWith = nameof(ShowBar))] + public PluginConfigColor RechargingBackgroundColor = new PluginConfigColor(new Vector4(150f / 255f, 0f / 255f, 0f / 255f, 25f / 100f)); + + [Checkbox("Use Job Colors")] + [Order(75, collapseWith = nameof(ShowBar))] + public bool UseJobColors = false; + + [Checkbox("Show Icon", spacing = true)] + [Order(80)] + public bool ShowIcon = true; + + [Checkbox("Show Icon Cooldown Animation")] + [Order(81, collapseWith = nameof(ShowIcon))] + public bool ShowIconCooldownAnimation = false; + + [Checkbox("Change Icon Border When Active")] + [Order(82, collapseWith = nameof(ShowIcon))] + public bool ChangeIconBorderWhenActive = false; + + [ColorEdit4("Icon Active Border Color")] + [Order(83, collapseWith = nameof(ChangeIconBorderWhenActive))] + public PluginConfigColor IconActiveBorderColor = new PluginConfigColor(new Vector4(255f / 255f, 200f / 255f, 35f / 255f, 100f / 100f)); + + [DragInt("Icon Active Border Thickness", min = 1, max = 10)] + [Order(84, collapseWith = nameof(ChangeIconBorderWhenActive))] + public int IconActiveBorderThickness = 3; + + [Checkbox("Change Labels Color When Active", spacing = true)] + [Order(85)] + public bool ChangeLabelsColorWhenActive = false; + + [ColorEdit4("Labels Active Color")] + [Order(86, collapseWith = nameof(ChangeLabelsColorWhenActive))] + public PluginConfigColor LabelsActiveColor = new PluginConfigColor(new Vector4(255f / 255f, 200f / 255f, 35f / 255f, 100f / 100f)); + + [NestedConfig("Name Label", 100)] + public EditableLabelConfig NameLabel = new EditableLabelConfig(new Vector2(5, 0), "[name:initials]", DrawAnchor.Left, DrawAnchor.Left); + + [NestedConfig("Time Label", 105)] + public PartyCooldownTimeLabelConfig TimeLabel = new PartyCooldownTimeLabelConfig(new Vector2(-5, 0), "", DrawAnchor.Right, DrawAnchor.Right); + + public new static PartyCooldownsBarConfig DefaultConfig() + { + Vector2 size = new Vector2(150, 24); + + PartyCooldownsBarConfig config = new PartyCooldownsBarConfig(Vector2.Zero, size, PluginConfigColor.Empty); + + config.NameLabel.FontID = FontsConfig.DefaultMediumFontKey; + config.TimeLabel.FontID = FontsConfig.DefaultMediumFontKey; + config.TimeLabel.NumberFormat = 1; + + return config; + } + + public PartyCooldownsBarConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor, BarDirection fillDirection = BarDirection.Right) + : base(position, size, fillColor, fillDirection) + { + } + } + + public class PartyCooldownTimeLabelConfig : NumericLabelConfig + { + public PartyCooldownTimeLabelConfig(Vector2 position, string text, DrawAnchor frameAnchor, DrawAnchor textAnchor) + : base(position, text, frameAnchor, textAnchor) + { + } + + [Checkbox("Show Effect Duration", spacing = true)] + [Order(70)] + public bool ShowEffectDuration = true; + + [Checkbox("Show Remainin Cooldown")] + [Order(71)] + public bool ShowRemainingCooldown = true; + } + + [Exportable(false)] + [Disableable(false)] + [Section("Party Cooldowns", true)] + [SubSection("Cooldowns Tracked", 0)] + public class PartyCooldownsDataConfig : PluginConfigObject + { + public List Cooldowns = new List(); + + private List _removedIds = new() { 7398 }; + + private JobRoles _roleFilter = JobRoles.Unknown; + private uint _tankFilter = 0; + private uint _healerFilter = 0; + private uint _meleeFilter = 0; + private uint _rangedFilter = 0; + private uint _casterFilter = 0; + + private bool _needsPopupOpen = false; + private PartyCooldownData? _popupCooldown = null; + + public const int ColumnCount = 5; + + public delegate void CooldownsDataChangedEventHandler(PartyCooldownsDataConfig sender); + public event CooldownsDataChangedEventHandler? CooldownsDataChangedEvent; + + public delegate void CooldownsDataEnabledChangedEventHandler(PartyCooldownsDataConfig sender); + public event CooldownsDataEnabledChangedEventHandler? CooldownsDataEnabledChangedEvent; + + public new static PartyCooldownsDataConfig DefaultConfig() => new PartyCooldownsDataConfig(); + + private string[] _enabledOptions = new string[] { + "Enabled", + "Party Cooldowns Only", + "Party Frames Only", + "Disabled" + }; + + + public void UpdateDataIfNeeded() + { + bool needsSave = false; + + // remove old cooldowns that are not valid anymore + foreach (uint id in _removedIds) + { + PartyCooldownData? data = Cooldowns.FirstOrDefault(data => data.ActionId == id); + if (data != null) + { + Cooldowns.Remove(data); + } + } + + // update data using the game files + foreach (uint key in DefaultCooldowns.Keys) + { + PartyCooldownData? data = Cooldowns.FirstOrDefault(data => data.ActionId == key); + PartyCooldownData defaultData = DefaultCooldowns[key]; + + if (data == null) + { + Cooldowns.Add(defaultData); + needsSave = true; + } + else if (data != null && !data.Equals(defaultData)) + { + data.RequiredLevel = defaultData.RequiredLevel; + data.DisabledAfterLevel = defaultData.DisabledAfterLevel; + data.JobId = defaultData.JobId; + data.JobIds = defaultData.JobIds; + data.Role = defaultData.Role; + data.Roles = defaultData.Roles; + data.CooldownDuration = defaultData.CooldownDuration; + data.EffectDuration = defaultData.EffectDuration; + data.SharedActionIds = defaultData.SharedActionIds; + + needsSave = true; + } + } + + ExcelSheet? sheet = Plugin.DataManager.GetExcelSheet(); + ExcelSheet? descriptionsSheet = Plugin.DataManager.GetExcelSheet(); + + foreach (PartyCooldownData cooldown in Cooldowns) + { + LuminaAction? action = sheet?.GetRow(cooldown.ActionId); + if (!action.HasValue) { continue; } + + // get real cooldown from data + // keep hardcoded value for technical finish + if (action.Value.Recast100ms > 0 && cooldown.ActionId != 16004) + { + cooldown.CooldownDuration = action.Value.Recast100ms / 10; + } + + // not happy about this but didn't want to over-complicate things + // special case for troubadour, shield samba and tactician + if (cooldown.ActionId == 7405 || cooldown.ActionId == 16012 || cooldown.ActionId == 16889) + { + cooldown.OverriddenCooldownText = "90-120"; + } + + // reprisal, feint, addle + else if (cooldown.ActionId == 7535 || cooldown.ActionId == 7549 || cooldown.ActionId == 7560) + { + cooldown.OverriddenDurationText = "10-15"; + } + + // swiftcast + else if (cooldown.ActionId == 7561) + { + cooldown.OverriddenCooldownText = "40-60"; + } + + // special case for starry muse + if (cooldown.ActionId == 35349) + { + cooldown.IconId = 3826; + } + else + { + cooldown.IconId = action.Value.Icon; + } + + cooldown.Name = action.Value.Name.ToString(); + } + + if (needsSave) + { + ConfigurationManager.Instance.SaveConfigurations(true); + } + } + + [ManualDraw] + public bool Draw(ref bool changed) + { + ImGuiHelper.NewLineAndTab(); + + // filter + ImGui.SameLine(); + ImGui.Text("Filter: "); + + DrawFilter("All", JobRoles.Unknown); + DrawFilter("Tanks", JobRoles.Tank); + DrawFilter("Healers", JobRoles.Healer); + DrawFilter("Melee", JobRoles.DPSMelee); + DrawFilter("Ranged", JobRoles.DPSRanged); + DrawFilter("Casters", JobRoles.DPSCaster); + + DrawJobFilters(); + + // table + ImGuiHelper.NewLineAndTab(); + ImGui.SameLine(); + + var flags = + ImGuiTableFlags.RowBg | + ImGuiTableFlags.Borders | + ImGuiTableFlags.BordersOuter | + ImGuiTableFlags.BordersInner | + ImGuiTableFlags.ScrollY | + ImGuiTableFlags.SizingFixedSame; + + ExcelSheet? sheet = Plugin.DataManager.GetExcelSheet(); + var iconSize = new Vector2(30, 30); + + if (ImGui.BeginTable("##DelvUI_PartyCooldownsTable", 8, flags, new Vector2(900, 500))) + { + ImGui.TableSetupColumn("Enabled", ImGuiTableColumnFlags.WidthStretch, 22, 0); + ImGui.TableSetupColumn("Icon", ImGuiTableColumnFlags.WidthStretch, 5, 1); + ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch, 24, 2); + ImGui.TableSetupColumn("Cooldown", ImGuiTableColumnFlags.WidthStretch, 8, 3); + ImGui.TableSetupColumn("Duration", ImGuiTableColumnFlags.WidthStretch, 8, 4); + ImGui.TableSetupColumn("Priority", ImGuiTableColumnFlags.WidthStretch, 11, 5); + ImGui.TableSetupColumn("Section", ImGuiTableColumnFlags.WidthStretch, 11, 6); + ImGui.TableSetupColumn("Exclude Jobs", ImGuiTableColumnFlags.WidthStretch, 11, 7); + + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableHeadersRow(); + + foreach (PartyCooldownData cooldown in Cooldowns) + { + // apply filter + if (_roleFilter != JobRoles.Unknown) + { + if (!cooldown.HasRole(_roleFilter)) { continue; } + + if (_roleFilter == JobRoles.Tank && _tankFilter != 0 && !cooldown.IsUsableBy(_tankFilter)) { continue; } + if (_roleFilter == JobRoles.Healer && _healerFilter != 0 && !cooldown.IsUsableBy(_healerFilter)) { continue; } + if (_roleFilter == JobRoles.DPSMelee && _meleeFilter != 0 && !cooldown.IsUsableBy(_meleeFilter)) { continue; } + if (_roleFilter == JobRoles.DPSRanged && _rangedFilter != 0 && !cooldown.IsUsableBy(_rangedFilter)) { continue; } + if (_roleFilter == JobRoles.DPSCaster && _casterFilter != 0 && !cooldown.IsUsableBy(_casterFilter)) { continue; } + } + + LuminaAction? action = sheet?.GetRow(cooldown.ActionId); + + ImGui.PushID(cooldown.ActionId.ToString()); + ImGui.TableNextRow(ImGuiTableRowFlags.None, iconSize.Y); + + // enabled + if (ImGui.TableSetColumnIndex(0)) + { + ImGui.PushItemWidth(178); + ImGui.SetCursorPos(new Vector2(ImGui.GetCursorPosX() + 2, ImGui.GetCursorPosY() + 2)); + int enabled = (int)cooldown.EnabledV2; + if (ImGui.Combo($"##{cooldown.ActionId}_enabled", ref enabled, _enabledOptions)) + { + changed = true; + cooldown.EnabledV2 = (PartyCooldownEnabled)enabled; + CooldownsDataEnabledChangedEvent?.Invoke(this); + } + } + + // icon + if (ImGui.TableSetColumnIndex(1) && action != null) + { + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 3); + DrawHelper.DrawIcon(action, ImGui.GetCursorPos(), iconSize, false, false); + } + + // name + if (ImGui.TableSetColumnIndex(2)) + { + ImGui.Text(cooldown.Name); + } + + // cooldown + if (ImGui.TableSetColumnIndex(3)) + { + string cooldownText = cooldown.OverriddenCooldownText != null ? cooldown.OverriddenCooldownText : $"{cooldown.CooldownDuration}"; + ImGui.Text(cooldownText); + } + + // duration + if (ImGui.TableSetColumnIndex(4)) + { + string durationText = cooldown.OverriddenDurationText != null ? cooldown.OverriddenDurationText : $"{cooldown.EffectDuration}"; + ImGui.Text(durationText); + } + + // priority + if (ImGui.TableSetColumnIndex(5)) + { + ImGui.PushItemWidth(86); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 2); + + if (ImGui.DragInt($"##{cooldown.ActionId}_priority", ref cooldown.Priority, 1, 0, 100)) + { + cooldown.Priority = Math.Clamp(cooldown.Priority, 0, 100); + changed = true; + CooldownsDataChangedEvent?.Invoke(this); + } + + ImGuiHelper.SetTooltip("Priority determines which cooldows show first on the list."); + } + + // column + if (ImGui.TableSetColumnIndex(6)) + { + ImGui.PushItemWidth(86); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 2); + + if (ImGui.DragInt($"##{cooldown.ActionId}_column", ref cooldown.Column, 0.1f, 1, ColumnCount, "%i", ImGuiSliderFlags.NoInput)) + { + changed = true; + CooldownsDataChangedEvent?.Invoke(this); + } + + ImGuiHelper.SetTooltip("Allows to separate cooldowns in different columns."); + } + + // exlude + if (ImGui.TableSetColumnIndex(7) && (cooldown.Roles != null || cooldown.Role != JobRoles.Unknown)) + { + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 2); + + if (ImGui.Button("Select##Exclude", new Vector2(86, 24))) + { + _needsPopupOpen = true; + _popupCooldown = cooldown; + } + } + + ImGui.PopID(); + } + + ImGui.EndTable(); + } + + if (_needsPopupOpen) + { + ImGui.OpenPopup("Exclude Jobs##HSUI"); + _needsPopupOpen = false; + } + + if (_popupCooldown != null) + { + DrawJobsListForCooldown(_popupCooldown); + } + + return false; + } + + private List IgnoredJobIds = new List() + { + JobIDs.GLA, JobIDs.MRD, + JobIDs.CNJ, JobIDs.WHM, + JobIDs.PGL, JobIDs.LNC, JobIDs.ROG, + JobIDs.ARC, + JobIDs.THM, JobIDs.ACN, JobIDs.BLU, + }; + + private void DrawJobsListForCooldown(PartyCooldownData cooldown) + { + List roles = cooldown.Roles ?? new List() { cooldown.Role }; + + List jobIds = new List(); + foreach (JobRoles role in roles) + { + jobIds.AddRange(JobsHelper.JobsByRole[role]); + } + jobIds = jobIds.Where(id => !IgnoredJobIds.Contains(id)).ToList(); + + string title = cooldown.Name + ":"; + float width = Math.Max(100, ImGui.CalcTextSize(title).X + 10); + ImGui.SetNextWindowSize(new(width, jobIds.Count * 30 + 32)); + + if (ImGui.BeginPopup("Exclude Jobs##HSUI", ImGuiWindowFlags.NoMove)) + { + ImGui.Text(title); + + foreach (uint jobId in jobIds) + { + bool selected = !cooldown.ExcludedJobIds.Contains(jobId); + if (ImGui.Checkbox(JobsHelper.JobNames[jobId] + "##popup", ref selected)) + { + if (selected) + { + cooldown.ExcludedJobIds.Remove(jobId); + } + else + { + cooldown.ExcludedJobIds.Add(jobId); + } + + CooldownsDataEnabledChangedEvent?.Invoke(this); + } + } + + ImGui.EndPopup(); + } + } + + private void DrawFilter(string name, JobRoles role) + { + ImGui.SameLine(); + if (ImGui.RadioButton(name, _roleFilter == role)) + { + _roleFilter = role; + } + } + + private void DrawJobFilter(string name, uint job, ref uint filter) + { + ImGui.SameLine(); + if (ImGui.RadioButton(name, filter == job)) + { + filter = job; + } + } + + private void DrawJobFilters() + { + if (_roleFilter == JobRoles.Unknown) { return; } + + ImGui.Text("\t\t\t\t "); + + if (_roleFilter == JobRoles.Tank) + { + DrawJobFilter("All##tank", 0, ref _tankFilter); + DrawJobFilter(JobsHelper.JobNames[JobIDs.PLD], JobIDs.PLD, ref _tankFilter); + DrawJobFilter(JobsHelper.JobNames[JobIDs.WAR], JobIDs.WAR, ref _tankFilter); + DrawJobFilter(JobsHelper.JobNames[JobIDs.DRK], JobIDs.DRK, ref _tankFilter); + DrawJobFilter(JobsHelper.JobNames[JobIDs.GNB], JobIDs.GNB, ref _tankFilter); + } + else if (_roleFilter == JobRoles.Healer) + { + DrawJobFilter("All##healer", 0, ref _healerFilter); + DrawJobFilter(JobsHelper.JobNames[JobIDs.WHM], JobIDs.WHM, ref _healerFilter); + DrawJobFilter(JobsHelper.JobNames[JobIDs.SCH], JobIDs.SCH, ref _healerFilter); + DrawJobFilter(JobsHelper.JobNames[JobIDs.AST], JobIDs.AST, ref _healerFilter); + DrawJobFilter(JobsHelper.JobNames[JobIDs.SGE], JobIDs.SGE, ref _healerFilter); + } + else if (_roleFilter == JobRoles.DPSMelee) + { + DrawJobFilter("All##melee", 0, ref _meleeFilter); + DrawJobFilter(JobsHelper.JobNames[JobIDs.MNK], JobIDs.MNK, ref _meleeFilter); + DrawJobFilter(JobsHelper.JobNames[JobIDs.DRG], JobIDs.DRG, ref _meleeFilter); + DrawJobFilter(JobsHelper.JobNames[JobIDs.NIN], JobIDs.NIN, ref _meleeFilter); + DrawJobFilter(JobsHelper.JobNames[JobIDs.SAM], JobIDs.SAM, ref _meleeFilter); + DrawJobFilter(JobsHelper.JobNames[JobIDs.RPR], JobIDs.RPR, ref _meleeFilter); + DrawJobFilter(JobsHelper.JobNames[JobIDs.VPR], JobIDs.VPR, ref _meleeFilter); + } + else if (_roleFilter == JobRoles.DPSRanged) + { + DrawJobFilter("All##ranged", 0, ref _rangedFilter); + DrawJobFilter(JobsHelper.JobNames[JobIDs.BRD], JobIDs.BRD, ref _rangedFilter); + DrawJobFilter(JobsHelper.JobNames[JobIDs.MCH], JobIDs.MCH, ref _rangedFilter); + DrawJobFilter(JobsHelper.JobNames[JobIDs.DNC], JobIDs.DNC, ref _rangedFilter); + } + else if (_roleFilter == JobRoles.DPSCaster) + { + DrawJobFilter("All##caster", 0, ref _casterFilter); + DrawJobFilter(JobsHelper.JobNames[JobIDs.BLM], JobIDs.BLM, ref _casterFilter); + DrawJobFilter(JobsHelper.JobNames[JobIDs.SMN], JobIDs.SMN, ref _casterFilter); + DrawJobFilter(JobsHelper.JobNames[JobIDs.RDM], JobIDs.RDM, ref _casterFilter); + DrawJobFilter(JobsHelper.JobNames[JobIDs.PCT], JobIDs.PCT, ref _casterFilter); + } + } + + public static Dictionary DefaultCooldowns = new Dictionary() + { + // PARTY-WIDE EFFECTS + + // Default columns: + // 1. Role mitigations (Reprisal, Addle, Feint) + // 2. Job specific mitigations + // 3. Damage buffs / burst window cooldowns + // 4. Personal mitigations / immunities / externals + // 5. Misc (Provoke, Shirk, Switcast, Rescue, etc) + + // TANKS ------------------------------------------------------------------------------------------------- + [7535] = NewData(7535, JobRoles.Tank, 22, 60, 10, 100, 1, PartyCooldownEnabled.PartyFrames), // reprisal + [7531] = NewData(7531, JobRoles.Tank, 8, 90, 20, 50, 4, PartyCooldownEnabled.PartyFrames), // rampart + [7533] = NewData(7533, JobRoles.Tank, 15, 30, 1, 50, 5, PartyCooldownEnabled.PartyFrames), // provoke + [7537] = NewData(7537, JobRoles.Tank, 48, 120, 1, 50, 5, PartyCooldownEnabled.PartyFrames), // shirk + + // PLD + [30] = NewData(30, JobIDs.PLD, 50, 420, 10, 100, 4, PartyCooldownEnabled.PartyFrames), // hallowed ground + [3540] = NewData(3540, JobIDs.PLD, 56, 90, 30, 90, 2, PartyCooldownEnabled.PartyFrames), // divine veil + [7385] = NewData(7385, JobIDs.PLD, 70, 120, 18, 90, 2, PartyCooldownEnabled.PartyFrames), // passage of arms + [20] = NewData(20, JobIDs.PLD, 2, 60, 20, 10, 3, PartyCooldownEnabled.PartyFrames), // fight or flight + [22] = NewData(22, JobIDs.PLD, 52, 90, 10, 90, 4, PartyCooldownEnabled.PartyFrames), // bulwark + [27] = NewData(27, JobIDs.PLD, 45, 120, 12, 90, 4, PartyCooldownEnabled.PartyFrames), // cover + + [17] = NewData(17, JobIDs.PLD, 38, 120, 15, 90, 4, PartyCooldownEnabled.PartyFrames, disabledAfterLevel: 92), // sentinel + [36920] = NewData(36920, JobIDs.PLD, 92, 120, 15, 90, 4, PartyCooldownEnabled.PartyFrames), // guardian + + // WAR + [43] = NewData(43, JobIDs.WAR, 42, 240, 10, 100, 4, PartyCooldownEnabled.PartyFrames), // holmgang + [7388] = NewData(7388, JobIDs.WAR, 68, 90, 30, 90, 2, PartyCooldownEnabled.PartyFrames), // shake it off + [52] = NewData(52, JobIDs.WAR, 50, 60, 30, 10, 3, PartyCooldownEnabled.PartyFrames), // infuriate + [38] = NewData(38, JobIDs.WAR, 6, 60, 30, 10, 3, PartyCooldownEnabled.PartyFrames, disabledAfterLevel: 70), // berserk + [7389] = NewData(7389, JobIDs.WAR, 70, 60, 30, 10, 3, PartyCooldownEnabled.PartyFrames), // inner release + [40] = NewData(40, JobIDs.WAR, 30, 90, 10, 90, 4, PartyCooldownEnabled.PartyFrames), // thrill of battle + [3552] = NewData(3552, JobIDs.WAR, 58, 60, 15, 90, 4, PartyCooldownEnabled.PartyFrames), // equilibrium + + [3551] = NewData(3551, JobIDs.WAR, 56, 25, 8, 90, 4, PartyCooldownEnabled.PartyFrames, disabledAfterLevel: 82, sharedActionIds: new() { 16464 }), // raw intuition + [25751] = NewData(25751, JobIDs.WAR, 82, 25, 8, 90, 4, PartyCooldownEnabled.PartyFrames, sharedActionIds: new() { 16464 }), // bloodwhetting + [16464] = NewData(16464, JobIDs.WAR, 76, 25, 8, 90, 4, PartyCooldownEnabled.PartyFrames, sharedActionIds: new() { 3551, 25751 }), // nascent flash + + [44] = NewData(44, JobIDs.WAR, 38, 120, 15, 90, 4, PartyCooldownEnabled.PartyFrames, disabledAfterLevel: 92), // vengeance + [36923] = NewData(36923, JobIDs.WAR, 92, 120, 15, 90, 4, PartyCooldownEnabled.PartyFrames), // damnation + + // DRK + [3638] = NewData(3638, JobIDs.DRK, 50, 300, 10, 100, 4, PartyCooldownEnabled.PartyFrames), // living dead + [16471] = NewData(16471, JobIDs.DRK, 66, 90, 15, 90, 2, PartyCooldownEnabled.PartyFrames), // dark missionary + [16472] = NewData(16472, JobIDs.DRK, 80, 120, 20, 10, 3, PartyCooldownEnabled.PartyFrames), // living shadow + [3634] = NewData(3634, JobIDs.DRK, 45, 60, 10, 90, 4, PartyCooldownEnabled.PartyFrames), // dark mind + [25754] = NewData(25754, JobIDs.DRK, 82, 60, 10, 90, 4, PartyCooldownEnabled.PartyFrames), // oblation + + [3625] = NewData(3625, JobIDs.DRK, 35, 60, 15, 10, 3, PartyCooldownEnabled.PartyFrames, disabledAfterLevel: 68), // blood weapon + [7390] = NewData(7390, JobIDs.DRK, 68, 60, 15, 10, 3, PartyCooldownEnabled.PartyFrames), // delirium + + [3636] = NewData(3636, JobIDs.DRK, 38, 120, 15, 90, 4, PartyCooldownEnabled.PartyFrames, disabledAfterLevel: 92), // shadow wall + [36927] = NewData(36927, JobIDs.DRK, 92, 120, 15, 90, 4, PartyCooldownEnabled.PartyFrames), // shadowed vigil + + // GNB + [16152] = NewData(16152, JobIDs.GNB, 50, 360, 10, 100, 4, PartyCooldownEnabled.PartyFrames), // superbolide + [16160] = NewData(16160, JobIDs.GNB, 64, 90, 15, 90, 2, PartyCooldownEnabled.PartyFrames), // heart of light + [16164] = NewData(16164, JobIDs.GNB, 76, 120, 1, 10, 3, PartyCooldownEnabled.PartyFrames), // bloodfest + [16138] = NewData(16138, JobIDs.GNB, 2, 60, 20, 10, 3, PartyCooldownEnabled.PartyFrames), // no mercy + [16140] = NewData(16140, JobIDs.GNB, 6, 90, 20, 90, 4, PartyCooldownEnabled.PartyFrames), // camouflage + + [16161] = NewData(16161, JobIDs.GNB, 68, 25, 7, 90, 4, PartyCooldownEnabled.PartyFrames, disabledAfterLevel: 82), // heart of stone + [25758] = NewData(25758, JobIDs.GNB, 82, 25, 7, 90, 4, PartyCooldownEnabled.PartyFrames), // heart of corundum + + [16148] = NewData(16148, JobIDs.GNB, 38, 120, 15, 90, 4, PartyCooldownEnabled.PartyFrames, disabledAfterLevel: 92), // nebula + [36935] = NewData(36935, JobIDs.GNB, 92, 120, 15, 90, 4, PartyCooldownEnabled.PartyFrames), // great nebula + + // HEALER ------------------------------------------------------------------------------------------------- + [7571] = NewData(7571, JobRoles.Healer, 48, 120, 0, 80, 5, PartyCooldownEnabled.Disabled), // rescue + + // AST + [16552] = NewData(16552, JobIDs.AST, 50, 120, 20, 30, 3, PartyCooldownEnabled.PartyCooldowns), // divination + [3613] = NewData(3613, JobIDs.AST, 58, 60, 18, 80, 2, PartyCooldownEnabled.PartyFrames), // collective unconscious + [16553] = NewData(16553, JobIDs.AST, 60, 60, 15, 80, 2, PartyCooldownEnabled.PartyFrames), // celestial opposition + [7439] = NewData(7439, JobIDs.AST, 62, 60, 20, 80, 2, PartyCooldownEnabled.PartyFrames), // earthly star (stellar detonation = 8324) + [16559] = NewData(16559, JobIDs.AST, 80, 120, 20, 80, 2, PartyCooldownEnabled.PartyFrames), // neutral sect + [25873] = NewData(25873, JobIDs.AST, 86, 60, 8, 80, 2, PartyCooldownEnabled.PartyFrames), // exaltation + [25874] = NewData(25874, JobIDs.AST, 90, 180, 15, 80, 2, PartyCooldownEnabled.PartyFrames), // macrocosmos (microcosmos = 25875) + [3606] = NewData(3606, JobIDs.AST, 6, 90, 15, 10, 4, PartyCooldownEnabled.PartyFrames), // lightspeed + + // SCH + [7436] = NewData(7436, JobIDs.SCH, 66, 120, 20, 30, 3, PartyCooldownEnabled.PartyCooldowns), // chain stratagem + [805] = NewData(805, JobIDs.SCH, 40, 120, 20, 50, 2, PartyCooldownEnabled.PartyFrames), // fey illumination + [188] = NewData(188, JobIDs.SCH, 50, 30, 15, 80, 2, PartyCooldownEnabled.PartyFrames), // sacred soil + [3585] = NewData(3585, JobIDs.SCH, 56, 90, 1, 80, 2, PartyCooldownEnabled.PartyFrames), // deployment tactics + [25867] = NewData(25867, JobIDs.SCH, 86, 60, 10, 80, 2, PartyCooldownEnabled.PartyFrames), // protraction + [25868] = NewData(25868, JobIDs.SCH, 90, 120, 20, 80, 2, PartyCooldownEnabled.PartyFrames), // expedient + [7434] = NewData(7434, JobIDs.SCH, 62, 45, 1, 50, 4, PartyCooldownEnabled.PartyFrames), // excogitation + [37014] = NewData(37014, JobIDs.SCH, 100, 180, 20, 80, 2, PartyCooldownEnabled.PartyFrames), // seraphism + + // WHM + [16536] = NewData(16536, JobIDs.WHM, 80, 120, 20, 80, 2, PartyCooldownEnabled.PartyFrames), // temperance + [3569] = NewData(3569, JobIDs.WHM, 52, 90, 24, 50, 2, PartyCooldownEnabled.PartyFrames), // asylum + [25862] = NewData(25862, JobIDs.WHM, 90, 180, 15, 80, 2, PartyCooldownEnabled.PartyFrames), // liturgy of the bell + [136] = NewData(136, JobIDs.WHM, 30, 120, 15, 10, 4, PartyCooldownEnabled.PartyFrames), // presence of mind + [140] = NewData(140, JobIDs.WHM, 50, 180, 1, 10, 4, PartyCooldownEnabled.PartyFrames), // benediction + [3570] = NewData(3570, JobIDs.WHM, 60, 60, 1, 10, 4, PartyCooldownEnabled.PartyFrames), // tetragrammaton + [25861] = NewData(25861, JobIDs.WHM, 86, 60, 8, 10, 4, PartyCooldownEnabled.PartyFrames), // aquaveil + + // SGE + [24298] = NewData(24298, JobIDs.SGE, 50, 30, 15, 80, 2, PartyCooldownEnabled.PartyFrames), // kerachole + [24302] = NewData(24302, JobIDs.SGE, 60, 60, 15, 80, 2, PartyCooldownEnabled.PartyFrames), // physis ii + [24310] = NewData(24310, JobIDs.SGE, 76, 120, 20, 80, 2, PartyCooldownEnabled.PartyFrames), // holos + [24311] = NewData(24311, JobIDs.SGE, 80, 120, 15, 80, 2, PartyCooldownEnabled.PartyFrames), // panhaima + [24318] = NewData(24318, JobIDs.SGE, 90, 120, 1, 80, 2, PartyCooldownEnabled.PartyFrames), // pneuma + [24303] = NewData(24303, JobIDs.SGE, 62, 45, 15, 10, 4, PartyCooldownEnabled.PartyFrames), // taurochole + [24305] = NewData(24305, JobIDs.SGE, 70, 120, 15, 10, 4, PartyCooldownEnabled.PartyFrames), // haima + [24317] = NewData(24317, JobIDs.SGE, 86, 60, 10, 10, 4, PartyCooldownEnabled.PartyFrames), // krasis + [37035] = NewData(37035, JobIDs.SGE, 100, 180, 20, 80, 2, PartyCooldownEnabled.PartyFrames), // philosophia + + // MELEE ------------------------------------------------------------------------------------------------- + [7549] = NewData(7549, JobRoles.DPSMelee, 22, 90, 10, 100, 1, PartyCooldownEnabled.PartyFrames), // feint + [7542] = NewData(7542, JobRoles.DPSMelee, 12, 90, 20, 10, 4, PartyCooldownEnabled.PartyFrames), // bloodbath + + // SAM + // lol? + + // VPR + // lol? + + // NIN + [2248] = NewData(2248, JobIDs.NIN, 15, 120, 20, 30, 3, PartyCooldownEnabled.PartyFrames, disabledAfterLevel: 68), // mug + [36957] = NewData(36957, JobIDs.NIN, 68, 120, 20, 30, 3, PartyCooldownEnabled.PartyCooldowns), // dokumori + [2258] = NewData(2258, JobIDs.NIN, 18, 60, 15, 10, 3, PartyCooldownEnabled.PartyFrames, disabledAfterLevel: 92), // trick attack + [36958] = NewData(36958, JobIDs.NIN, 92, 60, 15, 10, 3, PartyCooldownEnabled.PartyFrames), // kunai's bane + [2241] = NewData(2241, JobIDs.NIN, 2, 120, 20, 20, 4, PartyCooldownEnabled.PartyFrames), // shade shift + + // DRG + [3557] = NewData(3557, JobIDs.DRG, 52, 120, 20, 30, 3, PartyCooldownEnabled.PartyCooldowns), // battle litany + [85] = NewData(85, JobIDs.DRG, 30, 60, 20, 10, 3, PartyCooldownEnabled.PartyFrames), // lance charge + + // MNK + [65] = NewData(65, JobIDs.MNK, 42, 90, 15, 50, 2, PartyCooldownEnabled.PartyFrames), // mantra + [7396] = NewData(7396, JobIDs.MNK, 70, 120, 20, 90, 3, PartyCooldownEnabled.PartyCooldowns), // brotherhood + [7395] = NewData(7395, JobIDs.MNK, 68, 60, 20, 10, 3, PartyCooldownEnabled.PartyFrames), // riddle of fire + [7394] = NewData(7394, JobIDs.MNK, 64, 120, 15, 20, 4, PartyCooldownEnabled.PartyFrames), // riddle of earth + + // RPR + [24405] = NewData(24405, JobIDs.RPR, 72, 120, 20, 30, 3, PartyCooldownEnabled.PartyCooldowns), // arcane circle + [24404] = NewData(24404, JobIDs.RPR, 40, 30, 5, 10, 4, PartyCooldownEnabled.PartyFrames), // arcane crest + + // RANGED ------------------------------------------------------------------------------------------------- + // BRD + [118] = NewData(118, JobIDs.BRD, 50, 120, 20, 30, 3, PartyCooldownEnabled.PartyCooldowns), // battle voice + [7405] = NewData(7405, JobIDs.BRD, 62, 90, 15, 70, 2, PartyCooldownEnabled.PartyFrames, "90-120"), // troubadour + [7408] = NewData(7408, JobIDs.BRD, 66, 120, 15, 40, 2, PartyCooldownEnabled.PartyFrames), // nature's minne + [25785] = NewData(25785, JobIDs.BRD, 90, 110, 20, 30, 3, PartyCooldownEnabled.PartyCooldowns), // radiant finale + [101] = NewData(101, JobIDs.BRD, 4, 120, 20, 90, 3, PartyCooldownEnabled.PartyFrames), // raging strikes + + // DNC + [16012] = NewData(16012, JobIDs.DNC, 56, 90, 15, 70, 2, PartyCooldownEnabled.PartyFrames, "90-120"), // shield samba + [16004] = NewData(16004, JobIDs.DNC, 70, 120, 20, 30, 3, PartyCooldownEnabled.PartyCooldowns), // technical step / finish + [16011] = NewData(16011, JobIDs.DNC, 62, 120, 20, 90, 3, PartyCooldownEnabled.PartyFrames), // devilment + + // MCH + [16889] = NewData(16889, JobIDs.MCH, 56, 90, 15, 70, 2, PartyCooldownEnabled.PartyFrames, "90-120"), // tactician + [2887] = NewData(2887, JobIDs.MCH, 62, 120, 10, 70, 2, PartyCooldownEnabled.PartyFrames), // dismantle + + // CASTER ------------------------------------------------------------------------------------------------- + [7560] = NewData(7560, JobRoles.DPSCaster, 8, 90, 10, 100, 1, PartyCooldownEnabled.PartyFrames), // addle + + // RDM + [25857] = NewData(25857, JobIDs.RDM, 86, 120, 10, 70, 2, PartyCooldownEnabled.PartyCooldownsAndPartyFrames), // magick barrier + [7520] = NewData(7520, JobIDs.RDM, 58, 120, 20, 30, 3, PartyCooldownEnabled.PartyCooldowns), // embolden + + // SMN + [25801] = NewData(25801, JobIDs.SMN, 66, 120, 20, 30, 3, PartyCooldownEnabled.PartyCooldowns), // searing light + [25799] = NewData(25799, JobIDs.SMN, 2, 60, 30, 10, 3, PartyCooldownEnabled.PartyFrames), // radiant aegis + + // BLM + [3573] = NewData(3573, JobIDs.BLM, 52, 120, 30, 90, 3, PartyCooldownEnabled.PartyFrames), // ley lines + [157] = NewData(157, JobIDs.BLM, 38, 120, 20, 10, 4, PartyCooldownEnabled.PartyFrames), // manaward + + // PCT + [35349] = NewData(35349, JobIDs.PCT, 70, 120, 20, 30, 3, PartyCooldownEnabled.PartyCooldowns), // scenic muse + [34685] = NewData(34685, JobIDs.PCT, 10, 120, 10, 10, 4, PartyCooldownEnabled.PartyFrames), // tempera coat + + // MULTI-ROLE ------------------------------------------------------------------------------------------------- + [7541] = NewData(7541, new List() { JobRoles.DPSMelee, JobRoles.DPSRanged }, 8, 120, 0, 80, 4, PartyCooldownEnabled.PartyFrames), // second wind + [7561] = NewData(7561, new List() { JobRoles.Healer, JobRoles.DPSCaster }, 18, 60, 1, 80, 5, PartyCooldownEnabled.PartyFrames, null, new HashSet() { JobIDs.BLM, JobIDs.SMN, JobIDs.RDM }), // swiftcast + [7562] = NewData(7562, new List() { JobRoles.Healer, JobRoles.DPSCaster }, 14, 60, 21, 80, 5, PartyCooldownEnabled.Disabled), // lucid dreaming + }; + + #region helpers + private static PartyCooldownData NewData( + uint actionId, + uint jobId, + uint level, + int cooldown, + int effectDuration, + int priority, + int column, + PartyCooldownEnabled enabled, + string? overriddenCooldownText = null, + HashSet? excludedJobIds = null, + uint disabledAfterLevel = 0, + List? sharedActionIds = null) + { + PartyCooldownData data = NewData( + actionId, + level, + cooldown, + effectDuration, + priority, + column, + enabled, + overriddenCooldownText, + excludedJobIds, + disabledAfterLevel, + sharedActionIds + ); + + data.JobId = jobId; + data.Role = JobRoles.Unknown; + + return data; + } + + private static PartyCooldownData NewData(uint actionId, JobRoles role, uint level, int cooldown, int effectDuration, int priority, int column, PartyCooldownEnabled enabled, string? overriddenCooldownText = null, HashSet? excludedJobIds = null) + { + PartyCooldownData data = NewData( + actionId, + level, + cooldown, + effectDuration, + priority, + column, + enabled, + overriddenCooldownText, + excludedJobIds + ); + + data.JobId = 0; + data.Role = role; + + return data; + } + + private static PartyCooldownData NewData(uint actionId, List roles, uint level, int cooldown, int effectDuration, int priority, int column, PartyCooldownEnabled enabled, string? overriddenCooldownText = null, HashSet? excludedJobIds = null) + { + PartyCooldownData data = NewData( + actionId, + level, + cooldown, + effectDuration, + priority, + column, + enabled, + overriddenCooldownText, + excludedJobIds + ); + + data.JobId = 0; + data.Role = JobRoles.Unknown; + data.Roles = roles; + + return data; + } + + private static PartyCooldownData NewData( + uint actionId, + uint level, + int cooldown, + int effectDuration, + int priority, + int column, + PartyCooldownEnabled enabled, + string? overriddenCooldownText = null, + HashSet? excludedJobIds = null, + uint disabledAfterLevel = 0, + List? sharedActionIds = null + ) + { + PartyCooldownData data = new PartyCooldownData(); + data.ActionId = actionId; + data.RequiredLevel = level; + data.DisabledAfterLevel = disabledAfterLevel; + data.CooldownDuration = cooldown; + data.EffectDuration = effectDuration; + data.Priority = priority; + data.Column = column; + data.EnabledV2 = enabled; + data.OverriddenCooldownText = overriddenCooldownText; + data.SharedActionIds = sharedActionIds ?? new(); + + if (excludedJobIds != null) + { + data.ExcludedJobIds = excludedJobIds; + } + + return data; + } + #endregion + } +} + diff --git a/Interface/PartyCooldowns/PartyCooldownsHud.cs b/Interface/PartyCooldowns/PartyCooldownsHud.cs new file mode 100644 index 0000000..7a2c1dc --- /dev/null +++ b/Interface/PartyCooldowns/PartyCooldownsHud.cs @@ -0,0 +1,334 @@ +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Objects.Types; +using HSUI.Config; +using HSUI.Enums; +using HSUI.Helpers; +using HSUI.Interface.Bars; +using HSUI.Interface.GeneralElements; +using Dalamud.Bindings.ImGui; +using System; +using System.Collections.Generic; +using System.Numerics; + +namespace HSUI.Interface.PartyCooldowns +{ + public class PartyCooldownsHud : DraggableHudElement, IHudElementWithPreview, IHudElementWithVisibilityConfig + { + private PartyCooldownsConfig Config => (PartyCooldownsConfig)_config; + public VisibilityConfig VisibilityConfig => Config.VisibilityConfig; + private PartyCooldownsBarConfig _barConfig = null!; + private PartyCooldownsDataConfig _dataConfig = null!; + + private List> _cooldowns = new List>(); + + private LabelHud _nameLabelHud; + private LabelHud _timeLabelHud; + + public PartyCooldownsHud(PartyCooldownsConfig config, string displayName) : base(config, displayName) + { + _barConfig = ConfigurationManager.Instance.GetConfigObject(); + _dataConfig = ConfigurationManager.Instance.GetConfigObject(); + + _dataConfig.CooldownsDataChangedEvent += OnCooldownsDataChanged; + PartyCooldownsManager.Instance.CooldownsChangedEvent += OnCooldownsChanged; + + _nameLabelHud = new LabelHud(_barConfig.NameLabel); + _timeLabelHud = new LabelHud(_barConfig.TimeLabel); + + UpdateCooldowns(); + } + + protected override void InternalDispose() + { + _dataConfig.CooldownsDataChangedEvent -= OnCooldownsDataChanged; + PartyCooldownsManager.Instance.CooldownsChangedEvent -= OnCooldownsChanged; + } + + public void StopPreview() + { + Config.Preview = false; + PartyCooldownsManager.Instance?.UpdatePreview(); + } + + private void OnCooldownsDataChanged(PartyCooldownsDataConfig sender) + { + UpdateCooldowns(); + } + + private void OnCooldownsChanged(PartyCooldownsManager sender) + { + UpdateCooldowns(); + } + + private void UpdateCooldowns() + { + _cooldowns.Clear(); + + int columnCount = PartyCooldownsDataConfig.ColumnCount; + for (int i = 0; i < columnCount; i++) + { + _cooldowns.Add(new List()); + } + + foreach (Dictionary memberCooldownList in PartyCooldownsManager.Instance.CooldownsMap.Values) + { + foreach (PartyCooldown cooldown in memberCooldownList.Values) + { + if (!cooldown.Data.IsEnabledForPartyCooldowns()) { continue; } + + int columnIndex = Math.Min(columnCount - 1, cooldown.Data.Column - 1); + _cooldowns[columnIndex].Add(cooldown); + } + } + + foreach (List list in _cooldowns) + { + list.Sort((a, b) => + { + if (a.Data.Priority == b.Data.Priority) + { + if (a.Data.ActionId == b.Data.ActionId) + { + return a.SourceId.CompareTo(b.SourceId); + } + else + { + return a.Data.ActionId.CompareTo(b.Data.ActionId); + } + } + + if (a.Data.Priority < b.Data.Priority) + { + return -1; + } + + return 1; + }); + } + } + + protected override (List, List) ChildrenPositionsAndSizes() + { + // hardcoded just for draggable area purposes + const int columnCount = 3; + const int rowCount = 4; + + Vector2 size = new Vector2( + _barConfig.Size.X * columnCount + Config.Padding.X * (columnCount - 1), + _barConfig.Size.Y * rowCount + Config.Padding.Y * (rowCount - 1)); + + Vector2 pos = Config.GrowthDirection == PartyCooldownsGrowthDirection.Down ? Config.Position : Config.Position - new Vector2(0, size.Y); + + return (new List() { pos + size / 2f }, new List() { size }); + } + + public override void DrawChildren(Vector2 origin) + { + if (!Config.Enabled) { return; } + + IPlayerCharacter? player = Plugin.ObjectTable.LocalPlayer; + if (player == null) { return; } + + float offset = 0; + bool addedOffset = true; + bool isVertical = Config.GrowthDirection == PartyCooldownsGrowthDirection.Up || Config.GrowthDirection == PartyCooldownsGrowthDirection.Down; + + foreach (List list in _cooldowns) + { + if (list.Count == 0) { continue; } + + int i = 0; + + foreach (PartyCooldown cooldown in list) + { + if (!addedOffset) + { + offset += isVertical ? Config.Padding.X + _barConfig.Size.X : Config.Padding.Y + _barConfig.Size.Y; + addedOffset = true; + } + + string barId = _barConfig.ID + $"_{offset + i}"; + + // cooldown bar + float cooldownTime = cooldown.CooldownTimeRemaining(); + float effectTime = cooldown.EffectTimeRemaining(); + + float max = effectTime > 0 ? cooldown.GetEffectDuration() : cooldown.GetCooldown(); + float current = effectTime > 0 ? effectTime : cooldownTime; + + float sizeX = Math.Max(1, _barConfig.Size.X - _barConfig.Size.Y); + Vector2 size = new Vector2(sizeX, _barConfig.Size.Y); + + Vector2 pos; + + if (isVertical) + { + int direction = Config.GrowthDirection == PartyCooldownsGrowthDirection.Down ? 1 : -1; + pos = new Vector2( + Config.Position.X + size.Y + offset - 1, + Config.Position.Y + i * direction * size.Y + i * direction * Config.Padding.Y + ); + } + else + { + int direction = Config.GrowthDirection == PartyCooldownsGrowthDirection.Right ? 1 : -1; + pos = new Vector2( + size.Y + Config.Position.X + i * direction * size.X + i * direction * size.Y + i * direction * Config.Padding.X, + Config.Position.Y + offset - 1 + ); + } + + if (_barConfig.ShowBar) + { + PluginConfigColor fillColor = effectTime > 0 ? _barConfig.AvailableColor : _barConfig.RechargingColor; + PluginConfigColor bgColor = effectTime > 0 || cooldownTime == 0 ? _barConfig.AvailableBackgroundColor : _barConfig.RechargingBackgroundColor; + + if (_barConfig.UseJobColors) + { + uint? jobId = GetJobId(cooldown, player); + if (jobId.HasValue) + { + PluginConfigColor jobColor = GlobalColors.Instance.SafeColorForJobId(jobId.Value); + PluginConfigColor bgJobColor = jobColor.WithAlpha(40f / 100f); + PluginConfigColor rechargeJobColor = jobColor.WithAlpha(25f / 100f); + PluginConfigColor nonActive = PluginConfigColor.FromHex(0x88000000); + fillColor = effectTime > 0 ? jobColor : rechargeJobColor; + bgColor = effectTime > 0 || cooldownTime == 0 ? bgJobColor : nonActive; + } + } + + Rect background = new Rect(pos, size, bgColor); + Rect fill = BarUtilities.GetFillRect(pos, size, _barConfig.FillDirection, fillColor, current, max); + + BarHud bar = new BarHud( + barId, + _barConfig.DrawBorder, + _barConfig.BorderColor, + _barConfig.BorderThickness, + DrawAnchor.TopLeft, + current: current, + max: max, + barTextureName: _barConfig.BarTextureName, + barTextureDrawMode: _barConfig.BarTextureDrawMode + ); + + bar.SetBackground(background); + bar.AddForegrounds(fill); + + AddDrawActions(bar.GetDrawActions(origin, _barConfig.StrataLevel)); + } + + // icon + if (_barConfig.ShowIcon) + { + Vector2 iconPos = origin + new Vector2(pos.X - size.Y + 1, pos.Y); + Vector2 iconSize = new Vector2(size.Y); + bool recharging = effectTime == 0 && cooldownTime > 0; + bool shouldDrawCooldown = ClipRectsHelper.Instance.GetClipRectForArea(iconPos, iconSize) == null; + + AddDrawAction(_barConfig.StrataLevel, () => + { + DrawHelper.DrawInWindow(barId + "_icon", iconPos, iconSize, false, (drawList) => + { + uint color = recharging ? 0xAAFFFFFF : 0xFFFFFFFF; + DrawHelper.DrawIcon(cooldown.Data.IconId, iconPos, iconSize, false, color, drawList); + + // cooldown + if (shouldDrawCooldown && + _barConfig.ShowIconCooldownAnimation && + effectTime == 0 && + cooldownTime > 0) + { + DrawHelper.DrawIconCooldown(iconPos, iconSize, cooldownTime, cooldown.GetCooldown(), drawList); + } + + if (_barConfig.DrawBorder) + { + bool active = effectTime > 0 && _barConfig.ChangeIconBorderWhenActive; + uint iconBorderColor = active ? _barConfig.IconActiveBorderColor.Base : _barConfig.BorderColor.Base; + int thickness = active ? _barConfig.IconActiveBorderThickness : _barConfig.BorderThickness; + drawList.AddRect(iconPos, iconPos + iconSize, iconBorderColor, 0, ImDrawFlags.None, thickness); + } + }); + }); + } + + // name + PluginConfigColor? labelColor = effectTime > 0 && _barConfig.ChangeLabelsColorWhenActive ? _barConfig.LabelsActiveColor : null; + + ICharacter? character = cooldown.Member?.Character; + if (character == null && cooldown.SourceId == player.GameObjectId) + { + character = player; + } + + Vector2 labelPos = origin + pos; + AddDrawAction(_barConfig.NameLabel.StrataLevel, () => + { + PluginConfigColor realColor = _barConfig.NameLabel.Color; + _barConfig.NameLabel.Color = labelColor ?? realColor; + + string? name = character == null ? "Fake Name" : null; + _nameLabelHud.Draw(labelPos, size, character, name); + + _barConfig.NameLabel.Color = realColor; + }); + + // time + AddDrawAction(_barConfig.TimeLabel.StrataLevel, () => + { + PluginConfigColor realColor = _barConfig.TimeLabel.Color; + _barConfig.TimeLabel.Color = labelColor ?? realColor; + _barConfig.TimeLabel.SetText(""); + + if (effectTime > 0) + { + if (_barConfig.TimeLabel.ShowEffectDuration) + { + _barConfig.TimeLabel.SetValue(effectTime); + } + } + else if (cooldownTime > 0) + { + if (_barConfig.TimeLabel.ShowRemainingCooldown) + { + _barConfig.TimeLabel.SetText(Utils.DurationToString(cooldownTime, _barConfig.TimeLabel.NumberFormat)); + } + } + + _timeLabelHud.Draw(labelPos, size, character); + _barConfig.TimeLabel.Color = realColor; + }); + + // tooltip + pos = origin + new Vector2(pos.X - size.Y + 1, pos.Y); + if (Config.ShowTooltips && ImGui.IsMouseHoveringRect(pos, pos + _barConfig.Size)) + { + TooltipsHelper.Instance.ShowTooltipOnCursor( + cooldown.TooltipText(), + cooldown.Data.Name, + cooldown.Data.ActionId + ); + } + + i++; + } + + addedOffset = false; + } + } + + private uint? GetJobId(PartyCooldown cooldown, IPlayerCharacter player) + { + uint jobId = cooldown.Data.JobId; + if (jobId != 0) { return jobId; } + + if (cooldown.Member != null) { return cooldown.Member.JobId; } + + if (cooldown.SourceId == player.GameObjectId) { return player.ClassJob.RowId; } + + ICharacter? chara = Plugin.ObjectTable.SearchById(cooldown.SourceId) as ICharacter; + return chara?.ClassJob.RowId; + } + } +} diff --git a/Interface/PartyCooldowns/PartyCooldownsManager.cs b/Interface/PartyCooldowns/PartyCooldownsManager.cs new file mode 100644 index 0000000..a9022e6 --- /dev/null +++ b/Interface/PartyCooldowns/PartyCooldownsManager.cs @@ -0,0 +1,412 @@ +using Dalamud.Game.ClientState.Conditions; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Hooking; +using HSUI.Config; +using HSUI.Helpers; +using HSUI.Interface.Party; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using Dalamud.Bindings.ImGui; +using System; +using System.Collections.Generic; +using System.Numerics; +using static FFXIVClientStructs.FFXIV.Client.Game.Character.ActionEffectHandler; + +namespace HSUI.Interface.PartyCooldowns +{ + public unsafe class PartyCooldownsManager + { + #region Singleton + public static PartyCooldownsManager Instance { get; private set; } = null!; + private PartyCooldownsConfig _config = null!; + private PartyCooldownsDataConfig _dataConfig = null!; + + private PartyCooldownsManager() + { + try + { + _onActionUsedHook = Plugin.GameInteropProvider.HookFromAddress( + ActionEffectHandler.MemberFunctionPointers.Receive, + OnActionUsed + ); + _onActionUsedHook?.Enable(); + } + catch + { + Plugin.Logger.Error("PartyCooldowns OnActionUsed Hook failed!!!"); + } + + try + { + _actorControlHook = Plugin.GameInteropProvider.HookFromSignature( + "E8 ?? ?? ?? ?? 0F B7 0B 83 E9 64", + OnActorControl + ); + _actorControlHook?.Enable(); + } + catch + { + Plugin.Logger.Error("PartyCooldowns OnActorControl Hook failed!!!"); + } + + PartyManager.Instance.MembersChangedEvent += OnMembersChanged; + ConfigurationManager.Instance.ResetEvent += OnConfigReset; + Plugin.JobChangedEvent += OnJobChanged; + Plugin.ClientState.TerritoryChanged += OnTerritoryChanged; + + ConfigReset(ConfigurationManager.Instance, false); + UpdatePreview(); + } + + public static void Initialize() + { + Instance = new PartyCooldownsManager(); + } + + ~PartyCooldownsManager() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + + _onActionUsedHook?.Disable(); + _onActionUsedHook?.Dispose(); + + _actorControlHook?.Disable(); + _actorControlHook?.Dispose(); + + PartyManager.Instance.MembersChangedEvent -= OnMembersChanged; + Plugin.JobChangedEvent -= OnJobChanged; + _config.ValueChangeEvent -= OnConfigPropertyChanged; + _dataConfig.CooldownsDataEnabledChangedEvent -= OnCooldownEnabledChanged; + Plugin.ClientState.TerritoryChanged -= OnTerritoryChanged; + + Instance = null!; + } + + private void OnConfigReset(ConfigurationManager sender) + { + ConfigReset(sender); + } + + private void ConfigReset(ConfigurationManager sender, bool forceUpdate = true) + { + if (_config != null) + { + _config.ValueChangeEvent -= OnConfigPropertyChanged; + } + + _config = sender.GetConfigObject(); + _config.ValueChangeEvent += OnConfigPropertyChanged; + + if (_dataConfig != null) + { + _dataConfig.CooldownsDataEnabledChangedEvent -= OnCooldownEnabledChanged; + } + + _dataConfig = sender.GetConfigObject(); + _dataConfig.CooldownsDataEnabledChangedEvent += OnCooldownEnabledChanged; + _dataConfig.UpdateDataIfNeeded(); + + if (forceUpdate) + { + ForcedUpdate(); + } + } + + #endregion Singleton + + private Hook? _onActionUsedHook; + + private delegate void ActorControlDelegate(uint entityId, uint type, uint buffID, uint direct, uint actionId, uint sourceId, uint arg7, uint arg8, uint arg9, uint arg10, ulong targetId, byte arg12); + private Hook? _actorControlHook; + + private Dictionary>? _oldMap; + private Dictionary> _cooldownsMap = new Dictionary>(); + public IReadOnlyDictionary> CooldownsMap => _cooldownsMap; + + private Dictionary _technicalStepMap = new Dictionary(); + + public delegate void PartyCooldownsChangedEventHandler(PartyCooldownsManager sender); + public event PartyCooldownsChangedEventHandler? CooldownsChangedEvent; + + private bool _wasInDuty = false; + + private void OnActorControl(uint entityId, uint type, uint buffID, uint direct, uint actionId, uint sourceId, uint arg7, uint arg8, uint arg9, uint arg10, ulong targetId, byte arg12) + { + _actorControlHook?.Original(entityId, type, buffID, direct, actionId, sourceId, arg7, arg8, arg9, arg10, targetId, arg12); + + // detect wipe fadeouts (not 100% reliable but good enough) + if (type == 0x4000000F) + { + ResetCooldowns(); + } + } + + private void ResetCooldowns() + { + foreach (uint actorId in _cooldownsMap.Keys) + { + foreach (PartyCooldown cooldown in _cooldownsMap[actorId].Values) + { + cooldown.LastTimeUsed = 0; + } + } + } + + private unsafe void OnActionUsed(uint actorId, Character* casterPtr, Vector3* targetPos, Header* header, TargetEffects* effects, GameObjectId* targetEntityIds) + { + _onActionUsedHook?.Original(actorId, casterPtr, targetPos, header, effects, targetEntityIds); + + // check if its an action + if ((ActionType)header->ActionType != ActionType.Action ) { return; } + + // check if its a member in the party + if (!_cooldownsMap.ContainsKey(actorId)) + { + // check if its a party member's pet + IGameObject? actor = Plugin.ObjectTable.SearchById(actorId); + + if (actor is IBattleNpc battleNpc && _cooldownsMap.ContainsKey(battleNpc.OwnerId)) + { + actorId = battleNpc.OwnerId; + } + else + { + actorId = 0; + } + } + + if (actorId <= 0) { return; } + + uint actionID = header->ActionId; + + // special case for starry muse > set id to scenic muse + if (actionID == 34675) + { + actionID = 35349; + } + + // special case for technical step / finish + // we detect when technical step is pressed and save the time + // so we can properly calculate the cooldown once finish is pressed + if (actionID == 16193 || actionID == 16194 || actionID == 16195 || actionID == 16196) + { + actionID = 16004; + } + + if (actionID == 15998) + { + _technicalStepMap[actorId] = ImGui.GetTime(); + } + else + { + // check if its an action we track + if (_cooldownsMap[actorId].TryGetValue(actionID, out PartyCooldown? cooldown) && cooldown != null) + { + // if its technical finish, we set the cooldown start time to + // the time when step was pressed + if (_technicalStepMap.TryGetValue(actorId, out double stepStartTime) && actionID == 16004) + { + cooldown.OverridenCooldownStartTime = stepStartTime; + _technicalStepMap.Remove(actorId); + } + + double now = ImGui.GetTime(); + cooldown.LastTimeUsed = now; + cooldown.IgnoreNextUse = false; + + foreach (uint id in cooldown.Data.SharedActionIds) + { + if (_cooldownsMap[actorId].TryGetValue(id, out PartyCooldown? sharedCooldown) && sharedCooldown != null) + { + sharedCooldown.LastTimeUsed = now; + sharedCooldown.IgnoreNextUse = true; + } + } + } + } + } + + public void ForcedUpdate() + { + OnMembersChanged(PartyManager.Instance); + } + + private void OnMembersChanged(PartyManager sender) + { + Plugin.Framework.RunOnFrameworkThread(() => + { + if (sender.Previewing || _config.Preview) { return; } + + _cooldownsMap.Clear(); + + if (_config.ShowOnlyInDuties && !Plugin.Condition[ConditionFlag.BoundByDuty]) + { + CooldownsChangedEvent?.Invoke(this); + return; + } + + // show when solo + if (sender.IsSoloParty() || sender.MemberCount == 0) + { + var player = Plugin.ObjectTable.LocalPlayer; + if (_config.ShowWhenSolo && player != null) + { + _cooldownsMap.Add((uint)player.GameObjectId, CooldownsForMember((uint)player.GameObjectId, player.ClassJob.RowId, player.Level, null)); + } + } + else if (!_config.ShowOnlyInDuties || Plugin.Condition[ConditionFlag.BoundByDuty]) + { + // add new members + foreach (IPartyFramesMember member in sender.GroupMembers) + { + if (member.ObjectId > 0) + { + _cooldownsMap.Add(member.ObjectId, CooldownsForMember(member)); + } + } + } + + CooldownsChangedEvent?.Invoke(this); + }); + } + + private Dictionary CooldownsForMember(IPartyFramesMember member) + { + return CooldownsForMember(member.ObjectId, member.JobId, member.Level, member); + } + + private Dictionary CooldownsForMember(uint objectId, uint jobId, uint level, IPartyFramesMember? member) + { + Dictionary cooldowns = new Dictionary(); + + foreach (PartyCooldownData data in _dataConfig.Cooldowns) + { + if (data.EnabledV2 != PartyCooldownEnabled.Disabled && + level >= data.RequiredLevel && + (data.DisabledAfterLevel == 0 || level < data.DisabledAfterLevel) && + data.IsUsableBy(jobId) && + !data.ExcludedJobIds.Contains(jobId)) + { + cooldowns.Add(data.ActionId, new PartyCooldown(data, objectId, level, member)); + } + } + + return cooldowns; + } + + #region events + private void OnConfigPropertyChanged(object sender, OnChangeBaseArgs args) + { + if (args.PropertyName == "Preview") + { + UpdatePreview(); + } + else if (args.PropertyName == "ShowWhenSolo" && PartyManager.Instance?.MemberCount == 0) + { + OnMembersChanged(PartyManager.Instance); + } + else if (args.PropertyName == "ShowOnlyInDuties" && PartyManager.Instance != null) + { + OnMembersChanged(PartyManager.Instance); + } + } + + private void OnJobChanged(uint jobId) + { + ForcedUpdate(); + } + + private void OnCooldownEnabledChanged(PartyCooldownsDataConfig config) + { + ForcedUpdate(); + } + + private void OnTerritoryChanged(ushort territoryId) + { + bool isInDuty = Plugin.Condition[ConditionFlag.BoundByDuty]; + if (_config.ShowOnlyInDuties && _wasInDuty != isInDuty) + { + ForcedUpdate(); + } + + _wasInDuty = isInDuty; + } + + public void UpdatePreview() + { + if (!_config.Preview) + { + if (_oldMap != null) + { + _cooldownsMap = _oldMap; + } + else + { + _cooldownsMap.Clear(); + } + + if (PartyManager.Instance.Previewing) + { + CooldownsChangedEvent?.Invoke(this); + } + else + { + OnMembersChanged(PartyManager.Instance); + } + return; + } + + if (PartyManager.Instance?.Previewing == false) + { + _oldMap = _cooldownsMap; + } + + _cooldownsMap.Clear(); + Random RNG = new Random((int)ImGui.GetTime()); + + for (uint i = 1; i < 9; i++) + { + Dictionary cooldowns = new Dictionary(); + + JobRoles role = i < 3 ? JobRoles.Tank : (i < 5 ? JobRoles.Healer : JobRoles.Unknown); + role = role == JobRoles.Unknown ? JobRoles.DPSMelee + RNG.Next(3) : role; + int jobCount = JobsHelper.JobsByRole[role].Count; + int jobIndex = RNG.Next(jobCount); + uint jobId = JobsHelper.JobsByRole[role][jobIndex]; + + _cooldownsMap.Add(i, CooldownsForMember(i, jobId, 90, null)); + + foreach (PartyCooldown cooldown in _cooldownsMap[i].Values) + { + int rng = RNG.Next(100); + if (rng > 80) + { + cooldown.LastTimeUsed = ImGui.GetTime() - 30; + } + else if (rng > 50) + { + cooldown.LastTimeUsed = ImGui.GetTime() + 1; + } + } + } + + CooldownsChangedEvent?.Invoke(this); + } + #endregion + } +} diff --git a/Interface/StatusEffects/CustomEffectsListHud.cs b/Interface/StatusEffects/CustomEffectsListHud.cs new file mode 100644 index 0000000..fb34eb0 --- /dev/null +++ b/Interface/StatusEffects/CustomEffectsListHud.cs @@ -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 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; + } + } +} diff --git a/Interface/StatusEffects/StatusEffectsListConfig.cs b/Interface/StatusEffects/StatusEffectsListConfig.cs new file mode 100644 index 0000000..6e2281b --- /dev/null +++ b/Interface/StatusEffects/StatusEffectsListConfig.cs @@ -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 List = new SortedList(); + + [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? sheet) + { + if (input.Length > 0 && sheet != null) + { + List statusToAdd = new List(); + + // 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 tmpList = new SortedList(); + + 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(); + 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(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 converter; + converter = new NewTypeFieldConverter("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? sheet = Plugin.DataManager.GetExcelSheet(); + 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 + } +} \ No newline at end of file diff --git a/Interface/StatusEffects/StatusEffectsListHud.cs b/Interface/StatusEffects/StatusEffectsListHud.cs new file mode 100644 index 0000000..075a490 --- /dev/null +++ b/Interface/StatusEffects/StatusEffectsListHud.cs @@ -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, List) ChildrenPositionsAndSizes() + { + Vector2 pos = LayoutHelper.CalculateStartPosition(Config.Position, Config.Size, LayoutHelper.GrowthDirectionsFromIndex(Config.Directions)); + return (new List() { pos + Config.Size / 2f }, new List() { Config.Size }); + } + + public void StopMouseover() + { + if (_wasHovering && NeedsSpecialInput) + { + InputsHelper.Instance.StopHandlingInputs(); + _wasHovering = false; + } + } + + private uint CalculateLayout(List 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 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 StatusEffectDataList(IGameObject? actor) + { + List list = new List(); + 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()?.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 OrderByMineOrPermanentFirst(List 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 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(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; + } + } +} diff --git a/Media/Fonts/Expressway.ttf b/Media/Fonts/Expressway.ttf new file mode 100644 index 0000000..d295ee2 Binary files /dev/null and b/Media/Fonts/Expressway.ttf differ diff --git a/Media/Fonts/Roboto-Black.ttf b/Media/Fonts/Roboto-Black.ttf new file mode 100644 index 0000000..43a00e0 Binary files /dev/null and b/Media/Fonts/Roboto-Black.ttf differ diff --git a/Media/Fonts/Roboto-Light.ttf b/Media/Fonts/Roboto-Light.ttf new file mode 100644 index 0000000..0e97751 Binary files /dev/null and b/Media/Fonts/Roboto-Light.ttf differ diff --git a/Media/Fonts/big-noodle-too.ttf b/Media/Fonts/big-noodle-too.ttf new file mode 100644 index 0000000..3158f4f Binary files /dev/null and b/Media/Fonts/big-noodle-too.ttf differ diff --git a/Media/Images/banner_short.png b/Media/Images/banner_short.png new file mode 100644 index 0000000..3781e43 Binary files /dev/null and b/Media/Images/banner_short.png differ diff --git a/Media/Images/banner_short_transparent.png b/Media/Images/banner_short_transparent.png new file mode 100644 index 0000000..80aa4ff Binary files /dev/null and b/Media/Images/banner_short_transparent.png differ diff --git a/Media/Images/banner_short_x150.png b/Media/Images/banner_short_x150.png new file mode 100644 index 0000000..a15a60a Binary files /dev/null and b/Media/Images/banner_short_x150.png differ diff --git a/Media/Images/deafened.png b/Media/Images/deafened.png new file mode 100644 index 0000000..3dfc994 Binary files /dev/null and b/Media/Images/deafened.png differ diff --git a/Media/Images/icon.png b/Media/Images/icon.png new file mode 100644 index 0000000..865eb8b Binary files /dev/null and b/Media/Images/icon.png differ diff --git a/Media/Images/muted.png b/Media/Images/muted.png new file mode 100644 index 0000000..a7f9831 Binary files /dev/null and b/Media/Images/muted.png differ diff --git a/Media/Images/speaking.png b/Media/Images/speaking.png new file mode 100644 index 0000000..5c590fe Binary files /dev/null and b/Media/Images/speaking.png differ diff --git a/Media/Images/test.png b/Media/Images/test.png new file mode 100644 index 0000000..adbf90d Binary files /dev/null and b/Media/Images/test.png differ diff --git a/Media/Images/textures/Aluminium.png b/Media/Images/textures/Aluminium.png new file mode 100644 index 0000000..8da0d38 Binary files /dev/null and b/Media/Images/textures/Aluminium.png differ diff --git a/Media/Images/textures/BantoBar.png b/Media/Images/textures/BantoBar.png new file mode 100644 index 0000000..5759e84 Binary files /dev/null and b/Media/Images/textures/BantoBar.png differ diff --git a/Media/Images/textures/Bars.png b/Media/Images/textures/Bars.png new file mode 100644 index 0000000..1627572 Binary files /dev/null and b/Media/Images/textures/Bars.png differ diff --git a/Media/Images/textures/Bumps.png b/Media/Images/textures/Bumps.png new file mode 100644 index 0000000..c0c067a Binary files /dev/null and b/Media/Images/textures/Bumps.png differ diff --git a/Media/Images/textures/Button.png b/Media/Images/textures/Button.png new file mode 100644 index 0000000..0850565 Binary files /dev/null and b/Media/Images/textures/Button.png differ diff --git a/Media/Images/textures/Charcoal.png b/Media/Images/textures/Charcoal.png new file mode 100644 index 0000000..5507937 Binary files /dev/null and b/Media/Images/textures/Charcoal.png differ diff --git a/Media/Images/textures/Cilo.png b/Media/Images/textures/Cilo.png new file mode 100644 index 0000000..344bd9b Binary files /dev/null and b/Media/Images/textures/Cilo.png differ diff --git a/Media/Images/textures/Cloud.png b/Media/Images/textures/Cloud.png new file mode 100644 index 0000000..ba45241 Binary files /dev/null and b/Media/Images/textures/Cloud.png differ diff --git a/Media/Images/textures/Dabs.png b/Media/Images/textures/Dabs.png new file mode 100644 index 0000000..250708f Binary files /dev/null and b/Media/Images/textures/Dabs.png differ diff --git a/Media/Images/textures/Diagonal.png b/Media/Images/textures/Diagonal.png new file mode 100644 index 0000000..b41e49d Binary files /dev/null and b/Media/Images/textures/Diagonal.png differ diff --git a/Media/Images/textures/Frost.png b/Media/Images/textures/Frost.png new file mode 100644 index 0000000..39321f3 Binary files /dev/null and b/Media/Images/textures/Frost.png differ diff --git a/Media/Images/textures/Glass.png b/Media/Images/textures/Glass.png new file mode 100644 index 0000000..4216ad1 Binary files /dev/null and b/Media/Images/textures/Glass.png differ diff --git a/Media/Images/textures/Glass2.png b/Media/Images/textures/Glass2.png new file mode 100644 index 0000000..8362a2c Binary files /dev/null and b/Media/Images/textures/Glass2.png differ diff --git a/Media/Images/textures/Glaze.png b/Media/Images/textures/Glaze.png new file mode 100644 index 0000000..1c046b3 Binary files /dev/null and b/Media/Images/textures/Glaze.png differ diff --git a/Media/Images/textures/Gloss.png b/Media/Images/textures/Gloss.png new file mode 100644 index 0000000..5b04166 Binary files /dev/null and b/Media/Images/textures/Gloss.png differ diff --git a/Media/Images/textures/Graphite.png b/Media/Images/textures/Graphite.png new file mode 100644 index 0000000..c8b5a22 Binary files /dev/null and b/Media/Images/textures/Graphite.png differ diff --git a/Media/Images/textures/Grid.png b/Media/Images/textures/Grid.png new file mode 100644 index 0000000..0435cff Binary files /dev/null and b/Media/Images/textures/Grid.png differ diff --git a/Media/Images/textures/Hatched.png b/Media/Images/textures/Hatched.png new file mode 100644 index 0000000..edb1da3 Binary files /dev/null and b/Media/Images/textures/Hatched.png differ diff --git a/Media/Images/textures/Healbot.png b/Media/Images/textures/Healbot.png new file mode 100644 index 0000000..5f274b7 Binary files /dev/null and b/Media/Images/textures/Healbot.png differ diff --git a/Media/Images/textures/LiteStep.png b/Media/Images/textures/LiteStep.png new file mode 100644 index 0000000..54fb74f Binary files /dev/null and b/Media/Images/textures/LiteStep.png differ diff --git a/Media/Images/textures/Lyfe.png b/Media/Images/textures/Lyfe.png new file mode 100644 index 0000000..139f066 Binary files /dev/null and b/Media/Images/textures/Lyfe.png differ diff --git a/Media/Images/textures/Minimalist.png b/Media/Images/textures/Minimalist.png new file mode 100644 index 0000000..a36907b Binary files /dev/null and b/Media/Images/textures/Minimalist.png differ diff --git a/Media/Images/textures/Otravi.png b/Media/Images/textures/Otravi.png new file mode 100644 index 0000000..0ac5a1e Binary files /dev/null and b/Media/Images/textures/Otravi.png differ diff --git a/Media/Images/textures/Rain.png b/Media/Images/textures/Rain.png new file mode 100644 index 0000000..8f9904a Binary files /dev/null and b/Media/Images/textures/Rain.png differ diff --git a/Media/Images/textures/Rocks.png b/Media/Images/textures/Rocks.png new file mode 100644 index 0000000..ee8c868 Binary files /dev/null and b/Media/Images/textures/Rocks.png differ diff --git a/Media/Images/textures/Round.png b/Media/Images/textures/Round.png new file mode 100644 index 0000000..5970c2b Binary files /dev/null and b/Media/Images/textures/Round.png differ diff --git a/Media/Images/textures/Runes.png b/Media/Images/textures/Runes.png new file mode 100644 index 0000000..9a0f887 Binary files /dev/null and b/Media/Images/textures/Runes.png differ diff --git a/Media/Images/textures/Skewed.png b/Media/Images/textures/Skewed.png new file mode 100644 index 0000000..b308a0d Binary files /dev/null and b/Media/Images/textures/Skewed.png differ diff --git a/Media/Images/textures/Smudge.png b/Media/Images/textures/Smudge.png new file mode 100644 index 0000000..df158eb Binary files /dev/null and b/Media/Images/textures/Smudge.png differ diff --git a/Media/Images/textures/Striped.png b/Media/Images/textures/Striped.png new file mode 100644 index 0000000..bf02e73 Binary files /dev/null and b/Media/Images/textures/Striped.png differ diff --git a/Media/Images/textures/Water.png b/Media/Images/textures/Water.png new file mode 100644 index 0000000..ea7ef1f Binary files /dev/null and b/Media/Images/textures/Water.png differ diff --git a/Media/Images/textures/Wisps.png b/Media/Images/textures/Wisps.png new file mode 100644 index 0000000..b15cb73 Binary files /dev/null and b/Media/Images/textures/Wisps.png differ diff --git a/Media/Images/textures/Xeon.png b/Media/Images/textures/Xeon.png new file mode 100644 index 0000000..3157f8a Binary files /dev/null and b/Media/Images/textures/Xeon.png differ diff --git a/Media/Profiles/Default.HSUI b/Media/Profiles/Default.HSUI new file mode 100644 index 0000000..55eadc8 --- /dev/null +++ b/Media/Profiles/Default.HSUI @@ -0,0 +1 @@ +|||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 \ No newline at end of file diff --git a/Media/Profiles/Default.delvui b/Media/Profiles/Default.delvui new file mode 100644 index 0000000..55eadc8 --- /dev/null +++ b/Media/Profiles/Default.delvui @@ -0,0 +1 @@ +|||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 \ No newline at end of file diff --git a/Plugin.cs b/Plugin.cs new file mode 100644 index 0000000..853a99f --- /dev/null +++ b/Plugin.cs @@ -0,0 +1,485 @@ +using Dalamud.Game; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.Command; +using Dalamud.Interface; +using Dalamud.Interface.Textures; +using Dalamud.Plugin; +using Dalamud.Plugin.Services; +using HSUI.Config; +using HSUI.Config.Profiles; +using HSUI.Helpers; +using HSUI.Interface; +using HSUI.Interface.GeneralElements; +using HSUI.Interface.Nameplates; +using HSUI.Interface.Party; +using HSUI.Interface.PartyCooldowns; +using Dalamud.Bindings.ImGui; +using KamiToolKit; +using System; +using System.IO; +using System.Reflection; + +namespace HSUI +{ + public class Plugin : IDalamudPlugin + { + public static IBuddyList BuddyList { get; private set; } = null!; + public static IClientState ClientState { get; private set; } = null!; + public static ICommandManager CommandManager { get; private set; } = null!; + public static ICondition Condition { get; private set; } = null!; + public static IDalamudPluginInterface PluginInterface { get; private set; } = null!; + public static IDataManager DataManager { get; private set; } = null!; + public static IFramework Framework { get; private set; } = null!; + public static IGameGui GameGui { get; private set; } = null!; + public static IJobGauges JobGauges { get; private set; } = null!; + public static IObjectTable ObjectTable { get; private set; } = null!; + public static ISigScanner SigScanner { get; private set; } = null!; + public static IGameInteropProvider GameInteropProvider { get; private set; } = null!; + public static ITargetManager TargetManager { get; private set; } = null!; + public static IUiBuilder UiBuilder { get; private set; } = null!; + public static IPartyList PartyList { get; private set; } = null!; + public static IPluginLog Logger { get; private set; } = null!; + public static ITextureProvider TextureProvider { get; private set; } = null!; + public static IAddonLifecycle AddonLifecycle { get; private set; } = null!; + public static IChatGui Chat { get; private set; } = null!; + public static ISeStringEvaluator SeStringEvaluator { get; private set; } = null!; + + public static ISharedImmediateTexture? BannerTexture; + + public static string AssemblyLocation { get; private set; } = ""; + public string Name => "HSUI"; + + public static string Version { get; private set; } = ""; + + private HudManager _hudManager = null!; + + public delegate void JobChangedEventHandler(uint jobId); + public static event JobChangedEventHandler? JobChangedEvent; + private uint _jobId = 0; + + public static double LoadTime = -1; + + public Plugin( + IBuddyList buddyList, + IClientState clientState, + ICommandManager commandManager, + ICondition condition, + IDalamudPluginInterface pluginInterface, + IDataManager dataManager, + IFramework framework, + IGameGui gameGui, + IJobGauges jobGauges, + IObjectTable objectTable, + IPartyList partyList, + ISigScanner sigScanner, + IGameInteropProvider gameInteropProvider, + ITargetManager targetManager, + IPluginLog logger, + ITextureProvider textureProvider, + IAddonLifecycle addonLifecycle, + IChatGui chat, + ISeStringEvaluator seStringEvaluator) + { + BuddyList = buddyList; + ClientState = clientState; + CommandManager = commandManager; + Condition = condition; + PluginInterface = pluginInterface; + DataManager = dataManager; + Framework = framework; + GameGui = gameGui; + JobGauges = jobGauges; + ObjectTable = objectTable; + PartyList = partyList; + SigScanner = sigScanner; + GameInteropProvider = gameInteropProvider; + TargetManager = targetManager; + UiBuilder = PluginInterface.UiBuilder; + Logger = logger; + TextureProvider = textureProvider; + AddonLifecycle = addonLifecycle; + Chat = chat; + SeStringEvaluator = seStringEvaluator; + + if (pluginInterface.AssemblyLocation.DirectoryName != null) + { + AssemblyLocation = pluginInterface.AssemblyLocation.DirectoryName + "\\"; + } + else + { + AssemblyLocation = Assembly.GetExecutingAssembly().Location; + } + + Version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "1.0.0.0"; + + KamiToolKitLibrary.Initialize(pluginInterface); + + FontsManager.Initialize(AssemblyLocation); + BarTexturesManager.Initialize(AssemblyLocation); + LoadBanner(); + + ConfigurationManager.Initialize(); + ProfilesManager.Initialize(); + ConfigurationManager.Instance.LoadOrInitializeFiles(); + + FontsManager.Instance.LoadConfig(); + BarTexturesManager.Instance.LoadConfig(); + + ClipRectsHelper.Initialize(); + GlobalColors.Initialize(); + InputsHelper.Initialize(); + NameplatesManager.Initialize(); + PartyManager.Initialize(); + PartyCooldownsManager.Initialize(); + PullTimerHelper.Initialize(); + ActionBarsManager.Initialize(); + TextTagsHelper.Initialize(); + TooltipsHelper.Initialize(); + PetRenamerHelper.Initialize(); + HonorificHelper.Initialize(); + WotsitHelper.Initialize(); + WhosTalkingHelper.Initialize(); + + _hudManager = new HudManager(); + + UiBuilder.Draw += Draw; + UiBuilder.OpenConfigUi += OpenConfigUi; + + FontsManager.Instance.BuildFonts(); + + CommandManager.AddHandler( + "/hsui", + new CommandInfo(PluginCommand) + { + HelpMessage = "Opens the HSUI configuration window.\n" + + "/hsui toggle → Toggles HUD visibility.\n" + + "/hsui show → Shows HUD.\n" + + "/hsui hide → Hides HUD.\n" + + "/hsui toggledefaulthud → Toggles the game's Job Gauges visibility.\n" + + "/hsui forcejob → Forces HSUI to show the HUD for the given Job short name.\n" + + "/hsui profile → Switch to the given profile.\n" + + "/hsui mouse → Toggles special input handling for extra mouse buttons when hovering HSUI elements.\n" + + "/hsui debug dragdrop → Toggles debug logging for action bar drag & drop (all hotbars). Logs SwapSlots before/after, release-outside, item payload resolution.\n" + + "/hsui debug tooltips → Toggles debug logging for tooltips.\n" + + "/hsui debug hud → Dumps HudLayout addon names and hashes to the log (for HUD hiding).\n" + + "/hsui debug hotbarslots → Dumps all hotbar slot CommandType/CommandId to the log (for SwapSlots diagnosis).\n" + + "/hsui debug macro → Dumps HotbarSlot + RaptureMacroModule.Macro memory for the slot (macro persistence).", + ShowInHelp = true + } + ); + + CommandManager.AddHandler( + "/hui", + new CommandInfo(PluginCommand) + { + HelpMessage = "Opens the HSUI configuration window.\n" + + "/hui toggle → Toggles HUD visibility.\n" + + "/hui show → Shows HUD.\n" + + "/hui hide → Hides HUD.\n" + + "/hui toggledefaulthud → Toggles the game's Job Gauges visibility.\n" + + "/hui forcejob → Forces HSUI to show the HUD for the given Job short name.\n" + + "/hui profile → Switch to the given profile.\n" + + "/hui mouse → Toggles special input handling for extra mouse buttons when hovering HSUI elements.", + ShowInHelp = true + } + ); + + WotsitHelper.Instance?.Update(); + + if (ConfigurationManager.Instance?.IsChangelogWindowOpened == false) + { + LoadTime = ImGui.GetTime(); + } + } + + public void Dispose() + { + Logger.Info("Starting HSUI Dispose v" + Version); + Dispose(true); + GC.SuppressFinalize(this); + } + + private void LoadBanner() + { + string bannerImage = Path.Combine(Path.GetDirectoryName(AssemblyLocation.TrimEnd('\\')) ?? "", "Media", "Images", "banner_short_x150.png"); + + if (File.Exists(bannerImage)) + { + try + { + BannerTexture = TextureProvider.GetFromFile(bannerImage); + } + catch (Exception ex) + { + Logger.Error($"Image failed to load. {bannerImage}\n\n{ex}"); + } + } + else + { + Logger.Debug($"Image doesn't exist. {bannerImage}"); + } + } + + private void PluginCommand(string command, string arguments) + { + var configManager = ConfigurationManager.Instance; + + if (configManager.IsConfigWindowOpened && !configManager.LockHUD) + { + configManager.LockHUD = true; + } + else + { + bool printHUDStatus = false; + + switch (arguments) + { + case "toggle": + ConfigurationManager.Instance.ShowHUD = !ConfigurationManager.Instance.ShowHUD; + printHUDStatus = true; + break; + + case "toggledefaulthud": + HUDOptionsConfig config = ConfigurationManager.Instance.GetConfigObject(); + config.HideDefaultJobGauges = !config.HideDefaultJobGauges; + string defaultJobGaugeStr = config.HideDefaultJobGauges ? "hidden" : "visible"; + Chat.Print($"Default Job Gauges are {defaultJobGaugeStr}."); + break; + + case "show": + ConfigurationManager.Instance.ShowHUD = true; + printHUDStatus = true; + break; + + case "hide": + ConfigurationManager.Instance.ShowHUD = false; + printHUDStatus = true; + break; + + case { } argument when argument.StartsWith("mouse"): + string[] mouseArgs = argument.Split(" "); + if (mouseArgs.Length > 1) + { + if (mouseArgs[1] == "on") + InputsHelper.Instance?.ToggleProxy(true); + else if (mouseArgs[1] == "off") + InputsHelper.Instance?.ToggleProxy(false); + } + string mouseStr = InputsHelper.Instance?.IsProxyEnabled == true ? "enabled" : "disabled"; + Chat.Print($"HSUI special mouse handling is currently {mouseStr}."); + break; + + case { } argument when argument.StartsWith("forcejob"): + string[] args = argument.Split(" "); + if (args.Length > 1) + { + if (args[1].Equals("off", StringComparison.OrdinalIgnoreCase)) + { + ForcedJob.Enabled = false; + return; + } + var job = typeof(JobIDs).GetField(args[1].ToUpperInvariant()); + if (job != null) + { + ForcedJob.Enabled = true; + ForcedJob.ForcedJobId = (uint)(job.GetValue(null) ?? JobIDs.ACN); + } + } + break; + + case { } argument when argument.StartsWith("profile"): + string[] profile = argument.Split(" ", 2); + if (profile.Length > 1) + { + ProfilesManager.Instance?.CheckUpdateSwitchCurrentProfile(profile[1]); + } + break; + + case "debug dragdrop": + case "debug drag": + var hotbarConfigs = configManager.GetObjects(); + bool newState = hotbarConfigs.Count == 0 || !hotbarConfigs.Exists(c => c.DebugDragDrop); + foreach (var bar in hotbarConfigs) + bar.DebugDragDrop = newState; + configManager.SaveConfigurations(); + Chat.Print($"HSUI drag-drop debug logging is {(newState ? "ON" : "OFF")} for all hotbars."); + break; + + case "debug tooltips": + var tooltipsConfig = configManager.GetConfigObject(); + tooltipsConfig.DebugTooltips = !tooltipsConfig.DebugTooltips; + configManager.SaveConfigurations(); + Chat.Print($"HSUI tooltip debug logging is {(tooltipsConfig.DebugTooltips ? "ON" : "OFF")}."); + break; + + case "debug hud": + HSUI.Helpers.HudLayoutHashHelper.DumpHudLayoutAddonsToLog(); + Chat.Print("HSUI: HudLayout addon names and hashes dumped to the log (Dalamud log window or dev plugin)."); + break; + + case "debug hotbarslots": + HSUI.Helpers.ActionBarsManager.Instance?.DumpSlotStateToLog(); + Chat.Print("HSUI: Hotbar slot state dumped to the log."); + break; + + case "debug macromenu": + HSUI.Helpers.ActionBarsManager.Instance?.DumpMacroMenuToLog(); + Chat.Print("HSUI: Macro menu state dumped to the log (open Macro menu first for best data)."); + break; + + case { } arg when arg.StartsWith("debug macro"): + var macroParts = arg.Split(" ", StringSplitOptions.RemoveEmptyEntries); + if (macroParts.Length >= 4 && int.TryParse(macroParts[2], out int macroBar) && int.TryParse(macroParts[3], out int macroSlot)) + { + HSUI.Helpers.ActionBarsManager.Instance?.DumpMacroSlotMemoryToLog(macroBar, macroSlot); + Chat.Print($"HSUI: Macro slot memory for bar {macroBar} slot {macroSlot} dumped to the log."); + } + else + Chat.Print("Usage: /hsui debug macro (e.g. /hsui debug macro 1 2)"); + break; + + default: + configManager.ToggleConfigWindow(); + break; + } + + if (printHUDStatus) + { + string hudStr = ConfigurationManager.Instance.ShowHUD ? "visible" : "hidden"; + Chat.Print($"HSUI HUD is {hudStr}."); + } + } + } + + private void UpdateJob() + { + var player = ObjectTable.LocalPlayer; + if (player is null) return; + + uint newJobId = player.ClassJob.RowId; + if (ForcedJob.Enabled) + newJobId = ForcedJob.ForcedJobId; + + if (_jobId != newJobId) + { + _jobId = newJobId; + JobChangedEvent?.Invoke(_jobId); + } + } + + private static bool _drawInProgress; + + private void Draw() + { + if (_drawInProgress) return; + _drawInProgress = true; + try + { + UpdateJob(); + + UiBuilder.OverrideGameCursor = false; + + ConfigurationManager.Instance.Draw(); + try { NameplatesManager.Instance?.Update(); } + catch (Exception ex) { Logger.Warning($"NameplatesManager.Update: {ex.Message}"); } + try { PartyManager.Instance?.Update(); } + catch (Exception ex) { Logger.Warning($"PartyManager.Update: {ex.Message}"); } + + try + { + using (FontsManager.Instance.PushDefaultFont()) + { + _hudManager?.Draw(_jobId); + } + } + catch (Exception e) + { + Logger.Error("Something went wrong!:\n" + e.StackTrace); + } + + InputsHelper.Instance.OnFrameEnd(); + } + finally + { + _drawInProgress = false; + } + } + + private void OpenConfigUi() + { + ConfigurationManager.Instance.ToggleConfigWindow(); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposing) return; + + // Stop UI callbacks first so no Draw/OpenConfigUi runs during teardown + try + { + Logger.Info("\tRemoving commands..."); + CommandManager.RemoveHandler("/hsui"); + CommandManager.RemoveHandler("/hui"); + } + catch (Exception e) { Logger.Error("Error removing commands: " + e.Message); } + + try + { + Logger.Info("\tUnsubscribing from UIBuilder events..."); + UiBuilder.Draw -= Draw; + UiBuilder.OpenConfigUi -= OpenConfigUi; + } + catch (Exception e) { Logger.Error("Error unsubscribing UIBuilder: " + e.Message); } + + try + { + Logger.Info("\tSaving configurations..."); + ConfigurationManager.Instance?.SaveConfigurations(true); + ConfigurationManager.Instance?.CloseConfigWindow(); + } + catch (Exception e) { Logger.Error("Error saving/closing config: " + e.Message); } + + TryDispose("InputsHelper", () => InputsHelper.Instance?.Dispose()); + TryDispose("HudManager", () => _hudManager?.Dispose()); + TryDispose("BarTexturesManager", () => BarTexturesManager.Instance?.Dispose()); + TryDispose("ClipRectsHelper", () => ClipRectsHelper.Instance?.Dispose()); + TryDispose("ExperienceHelper", () => ExperienceHelper.Instance?.Dispose()); + TryDispose("FontsManager", () => FontsManager.Instance?.Dispose()); + TryDispose("GlobalColors", () => GlobalColors.Instance?.Dispose()); + TryDispose("NameplatesManager", () => NameplatesManager.Instance?.Dispose()); + TryDispose("PartyCooldownsManager", () => PartyCooldownsManager.Instance?.Dispose()); + TryDispose("PartyManager", () => PartyManager.Instance?.Dispose()); + TryDispose("PullTimerHelper", () => PullTimerHelper.Instance?.Dispose()); + TryDispose("ActionBarsManager", () => ActionBarsManager.Instance?.Dispose()); + TryDispose("ProfilesManager", () => ProfilesManager.Instance?.Dispose()); + TryDispose("SpellHelper", () => SpellHelper.Instance?.Dispose()); + TryDispose("TooltipsHelper", () => TooltipsHelper.Instance?.Dispose()); + TryDispose("HonorificHelper", () => HonorificHelper.Instance?.Dispose()); + TryDispose("PetRenamerHelper", () => PetRenamerHelper.Instance?.Dispose()); + TryDispose("WotsitHelper", () => WotsitHelper.Instance?.Dispose()); + TryDispose("WhosTalkingHelper", () => WhosTalkingHelper.Instance?.Dispose()); + + try + { + Logger.Info("\tRebuilding fonts..."); + UiBuilder.FontAtlas.BuildFontsAsync(); + } + catch (Exception e) { Logger.Error("Error rebuilding fonts: " + e.Message); } + + try { KamiToolKitLibrary.Dispose(); } + catch (Exception e) { Logger.Error("Error disposing KamiToolKit: " + e.Message); } + + TryDispose("ConfigurationManager", () => ConfigurationManager.Instance?.Dispose()); + } + + private static void TryDispose(string name, Action dispose) + { + try + { + Logger.Info("\tDisposing " + name + "..."); + dispose(); + } + catch (Exception e) { Logger.Error("Error disposing " + name + ": " + e.Message); } + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..f1b66b1 --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +# HSUI + +**A modern HUD replacement for Final Fantasy XIV, built for customization.** + +HSUI provides a highly configurable HUD replacement, recreated from DelvUI using KamiToolKit, FFXIVClientStructs, and Dalamud. Replace and customize your entire in-game HUD with a clean, flexible interface. + +![HSUI Banner](Media/Images/banner_short.png) + +--- + +## Features + +- **Unit Frames** — Player, target, focus target, target of target with customizable health bars, labels, and icons +- **Castbars** — Player and target castbars with interrupt indicators +- **Job Gauges** — All job-specific gauges (DRK, WAR, MNK, NIN, etc.) +- **Party Frames** — Configurable party list with health, buffs, debuffs, and cooldowns +- **Nameplates** — Customizable enemy and friendly nameplates +- **Status Effects** — Player and target buff/debuff display +- **Enemy List** — Enhanced enemy list with debuff tracking +- **Hotbars** — Configurable action bars with: + - Multiple layout options (12×1, 6×2, 4×3, 3×4, 2×6, 1×12) + - Drag-and-drop from game UI (Actions, Macros, Inventory) + - Shift+drag to rearrange, release outside to clear + - Cooldown overlays and combo highlighting +- **Profiles** — Save and load configurations, import from DelvUI +- **Other Elements** — Experience bar, GCD indicator, limit break, pull timer, MP ticker + +--- + +## Installation + +### Via Custom Plugin Repository (Recommended for FC/local installs) + +1. Open **XIVLauncher** → **Dalamud Settings** → **Experimental** +2. Add a custom plugin repository: + ``` + https://raw.githubusercontent.com/Knack117/HSUI/main/pluginmaster.json + ``` +3. Open the **Plugin Installer**, enable "Show custom plugin repositories", and install **HSUI** + +### Manual Installation + +1. Download the latest release from [Releases](https://github.com/Knack117/HSUI/releases) +2. Extract the contents to your Dalamud plugins folder: + - `%AppData%\XIVLauncher\installedPlugins\HSUI\` + +--- + +## Commands + +| Command | Description | +|--------|-------------| +| `/hsui` | Open HSUI settings | +| `/hui` | Alias for `/hsui` | + +--- + +## Building from Source + +### Prerequisites + +- .NET 10 SDK +- [KamiToolKit](https://github.com/KamiToolKit/KamiToolKit) (cloned to `../repos/KamiToolKit-master/`) + +### Build + +```bash +dotnet build -c Release +``` + +The built plugin will be in `bin/Release/`. + +--- + +## Project Structure + +``` +HSUI/ +├── Config/ # Configuration system +├── Enums/ # Shared enumerations +├── Helpers/ # Utilities and managers +├── Interface/ # HUD elements and configs +├── Media/ # Fonts, images, default profiles +├── HSUI.json # Plugin manifest +├── changelog.md # Version history +└── README.md +``` + +--- + +## Credits + +- **DelvUI** — Original design and inspiration +- **KamiToolKit** — UI framework +- **FFXIVClientStructs** — Game data structures +- **Dalamud** — Plugin framework + +--- + +## License + +This project uses code from [MOActionPlugin](https://github.com/attickdoor/MOActionPlugin) (GPL-3.0). See individual files for copyright notices. diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..576faf5 --- /dev/null +++ b/changelog.md @@ -0,0 +1,6 @@ +# 1.0.0.0 +- Initial release. HSUI recreation of DelvUI using KamiToolKit, FFXIVClientStructs, and Dalamud. +- Full HUD replacement: unit frames, castbars, job gauges, nameplates, party frames, status effects, enemy list, and more. +- Profiles, config import/export, and /hsui, /hui commands. +- Configurable hotbars: drag-and-drop from game UI (Actions, Macros, Inventory), multiple bar layouts (12×1, 6×2, 4×3, 3×4, 2×6, 1×12), Shift+drag to rearrange, release outside to clear. +- Hotbar tooltips with correct display for GeneralActions (e.g. Teleport), macros, and combat actions. diff --git a/pluginmaster.json b/pluginmaster.json new file mode 100644 index 0000000..bf101b7 --- /dev/null +++ b/pluginmaster.json @@ -0,0 +1,23 @@ +[ + { + "Author": "Knack117", + "Name": "HSUI", + "Punchline": "A modern HUD replacement built for customization.", + "Description": "HSUI provides a highly configurable HUD replacement for FFXIV, recreated from DelvUI using KamiToolKit, FFXIVClientStructs, and Dalamud. Features unit frames, castbars, job gauges, nameplates, party frames, status effects, enemy list, configurable hotbars with drag-and-drop, and profiles.", + "Changelog": "", + "InternalName": "HSUI", + "AssemblyVersion": "1.0.0.0", + "RepoUrl": "https://github.com/Knack117/HSUI", + "ApplicableVersion": "any", + "Tags": ["UI", "HUD", "Unit Frames", "Nameplates", "Party Frames", "Hotbars"], + "CategoryTags": ["UI"], + "DalamudApiLevel": 14, + "IconUrl": "https://raw.githubusercontent.com/Knack117/HSUI/main/Media/Images/icon.png", + "ImageUrls": [], + "DownloadLinkInstall": "https://github.com/Knack117/HSUI/releases/download/v1.0.0.0/latest.zip", + "IsHide": false, + "IsTestingExclusive": false, + "DownloadLinkTesting": "https://github.com/Knack117/HSUI/releases/download/v1.0.0.0/latest.zip", + "DownloadLinkUpdate": "https://github.com/Knack117/HSUI/releases/download/v1.0.0.0/latest.zip" + } +]