Compare commits

...

4 Commits

Author SHA1 Message Date
KnackAtNite 9f9d5e1781 v1.0.8.22: Duty List hide-unless-hovered fix, tooltip gauge cost section
Made-with: Cursor
2026-03-02 19:40:33 -05:00
KnackAtNite ee7a2d71a0 Merge pull request 'fix: Duty List hide-unless-hovered + tooltip job gauge cost section formatting' (#3) from feature/improvement into main 2026-03-03 00:39:30 +00:00
Jorg 7a61e11e37 fix: Duty List hide-unless-hovered + tooltip job gauge cost section formatting
Duty List & Scenario Guide:
- Fix 'Hide unless hovered' visibility option not working. Override
  GetScreenBounds() in DutyListScenarioHud so the hover hit-test rect
  matches the drawn panel position exactly (same formula as draw code).

Tooltips (game-style formatting):
- Treat job gauge/resource costs as a new section with section label color
  (e.g. Soul Gauge Cost, Beast Gauge Cost, Lily Cost, Kenki, Ninki,
  Cartridge, Oath, Polyglot, Addersgall, Astral/Lunar Sign, Battery/Heat).
- In BuildFormattedActionTooltipBody: recognize gauge cost lines in the
  stats block and emit them as their own section (newline + green label).
- Fix tooltip break when stats and description share the first block:
  switch to description on first non-stats line instead of dropping it,
  so ability description text is never lost.

Made-with: Cursor
2026-03-02 13:33:30 -06:00
KnackAtNite 27e743c80f v1.0.8.21: Duty Information in duty, objective progress as X out of Y steps + checkmark, average wait from queue
Made-with: Cursor
2026-03-02 04:06:13 -05:00
12 changed files with 1549 additions and 17 deletions
+1
View File
@@ -708,6 +708,7 @@ namespace HSUI.Config
typeof(PullTimerConfig),
typeof(LimitBreakConfig),
typeof(MPTickerConfig),
typeof(DutyListScenarioConfig),
// Colors
typeof(TanksColorConfig),
+2
View File
@@ -40,6 +40,8 @@ public enum ElementKind : uint
EnemyList = 0xB8BD6685, // _EnemyList_a
// Misc HUD
ToDoList = 0xA29100D2, // _ToDoList_a (Duty List)
ScenarioTree = 0x88EE6357, // ScenarioTree_a (Scenario Guide)
ExperienceBar = 0x21E53CCE, // _Exp_a
LimitGauge = 0xC79F450A, // _LimitBreak_a
StatusEffects = 0x4A569616, // _Status_a
+3 -3
View File
@@ -9,9 +9,9 @@
<PropertyGroup>
<AssemblyName>HSUI</AssemblyName>
<AssemblyVersion>1.0.8.20</AssemblyVersion>
<FileVersion>1.0.8.20</FileVersion>
<InformationalVersion>1.0.8.20</InformationalVersion>
<AssemblyVersion>1.0.8.22</AssemblyVersion>
<FileVersion>1.0.8.22</FileVersion>
<InformationalVersion>1.0.8.22</InformationalVersion>
</PropertyGroup>
<PropertyGroup>
+1 -1
View File
@@ -2,7 +2,7 @@
"Author": "Knack117",
"Name": "HSUI",
"InternalName": "HSUI",
"AssemblyVersion": "1.0.8.20",
"AssemblyVersion": "1.0.8.22",
"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",
+3
View File
@@ -358,10 +358,13 @@ namespace HSUI.Helpers
{
if (!ClipRectsHelper.Instance.Enabled || ClipRectsHelper.Instance.Mode == WindowClippingMode.Performance)
{
if (!needsInput && !needsWindow)
{
drawAction(ImGui.GetWindowDrawList());
return;
}
}
windowFlags |= ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoBringToFrontOnFocus;
File diff suppressed because it is too large Load Diff
+37 -12
View File
@@ -463,8 +463,9 @@ namespace HSUI.Helpers
if (string.IsNullOrEmpty(description))
return result;
// Pattern: section labels to color (Additional Effect, Duration, X Effect, Maximum Charges, Blood Gauge Cost, Combo, roles)
var sectionRegex = new Regex(@"\b(Additional Effect:|Duration:|Maximum Charges:|Blood Gauge Cost:|Combo Action:|Combo Potency:|Combo Bonus:|Tank:|Melee:|Ranged:|Caster:|Healer:|[A-Za-z][A-Za-z0-9\s]+ Effect:)", RegexOptions.None);
// Pattern: section labels to color (Additional Effect, Duration, X Effect, Maximum Charges, job gauge costs, Combo, roles)
// Job gauge/resource costs: Soul (RPR), Blood/Beast (DRK/WAR), Kenki/Ninki (SAM/NIN), Cartridge (GNB), Oath (PLD), Lily (WHM), etc.
var sectionRegex = new Regex(@"\b(Additional Effect:|Duration:|Maximum Charges:|Blood Gauge Cost:|Soul Gauge Cost:|Beast Gauge Cost:|Kenki Gauge Cost:|Kenki Cost:|Ninki Gauge Cost:|Ninki Cost:|Cartridge Cost:|Oath Gauge Cost:|Lily Cost:|Polyglot Cost:|Addersgall Cost:|Addersting Cost:|Astral Sign Cost:|Lunar Sign Cost:|Battery Gauge Cost:|Heat Gauge Cost:|[A-Za-z][A-Za-z0-9\s]*\s+Cost:|Combo Action:|Combo Potency:|Combo Bonus:|Tank:|Melee:|Ranged:|Caster:|Healer:|[A-Za-z][A-Za-z0-9\s]+ Effect:)", RegexOptions.None);
// Pattern: "Grants X X Effect:" - captures status name X
var grantsRegex = new Regex(@"Grants\s+(.+?)\s+\1\s+Effect:", RegexOptions.Singleline);
@@ -579,25 +580,33 @@ namespace HSUI.Helpers
var secondaryColor = config.SecondaryLabelColor.Vector;
var blocks = body.Split(new[] { "\n\n" }, StringSplitOptions.None);
int descStart = 0;
bool hasStats = false;
// Stats block: may contain "Potency: X" and "Cast: ... | Recast: ..." on separate lines
// Job gauge/resource cost line pattern (e.g. "Soul Gauge Cost: 50", "Lily Cost: 1") - treat as own section with section color
var gaugeCostLineRegex = new Regex(@"^(.+?\s+Cost:)\s*(.*)$", RegexOptions.None);
// Stats block: may contain "Potency: X", "Cast: ... | Recast: ...", and job gauge cost lines.
// Once we hit a line that doesn't match any of these, treat it and the rest as description (don't drop it).
List<string> descBlocks = new List<string>();
bool inDescription = false;
for (int i = 0; i < blocks.Length; i++)
{
string block = blocks[i].Trim();
var lines = block.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
bool blockIsStats = false;
List<string> descLinesInBlock = new List<string>();
foreach (string line in lines)
{
string part = line.Trim();
if (inDescription)
{
descLinesInBlock.Add(part);
continue;
}
if (part.StartsWith("Potency:", StringComparison.OrdinalIgnoreCase))
{
if (hasStats) result.Add(new TooltipSegment("\n", textColor));
result.Add(new TooltipSegment("Potency:", secondaryColor));
result.Add(new TooltipSegment(" " + part.Substring(8).TrimStart(), textColor));
hasStats = true;
blockIsStats = true;
}
else if (part.Contains("Cast:") || part.Contains("Recast:"))
{
@@ -605,19 +614,35 @@ namespace HSUI.Helpers
var statsSegs = FormatActionStats(part, textColor, secondaryColor);
result.AddRange(statsSegs);
hasStats = true;
blockIsStats = true;
}
}
if (!blockIsStats)
else if (gaugeCostLineRegex.IsMatch(part))
{
descStart = i;
var match = gaugeCostLineRegex.Match(part);
string label = match.Groups[1].Value.TrimEnd();
string value = match.Groups[2].Value.Trim();
if (hasStats) result.Add(new TooltipSegment("\n", textColor));
result.Add(new TooltipSegment(label + (string.IsNullOrEmpty(value) ? "" : " "), sectionColor));
if (!string.IsNullOrEmpty(value))
result.Add(new TooltipSegment(value, textColor));
hasStats = true;
}
else
{
inDescription = true;
descLinesInBlock.Add(part);
}
}
if (descLinesInBlock.Count > 0)
descBlocks.Add(string.Join("\n", descLinesInBlock));
if (inDescription)
{
descBlocks.AddRange(blocks.Skip(i + 1).Select(b => b.Trim()).Where(b => b.Length > 0));
break;
}
descStart = i + 1;
}
// Description: exactly one newline after stats, then description with compact section spacing
string desc = string.Join("\n", blocks.Skip(descStart)).Trim();
string desc = string.Join("\n\n", descBlocks).Trim();
if (!string.IsNullOrEmpty(desc))
{
if (hasStats) result.Add(new TooltipSegment("\n", textColor));
@@ -0,0 +1,72 @@
using Dalamud.Bindings.ImGui;
using HSUI.Config;
using HSUI.Config.Attributes;
using HSUI.Enums;
using System.Numerics;
namespace HSUI.Interface.GeneralElements
{
[Section("Other Elements")]
[SubSection("Duty List & Scenario Guide", 0)]
public class DutyListScenarioConfig : AnchorablePluginConfigObject
{
[Checkbox("Show Quest Icons")]
[Order(20)]
public bool ShowQuestIcons = true;
[DragInt("Icon Size", min = 12, max = 48)]
[Order(21, collapseWith = nameof(ShowQuestIcons))]
public int IconSize = 24;
[DragFloat("Font Scale", min = 0.5f, max = 2f)]
[Order(23)]
public float FontScale = 1f;
[Font("Font")]
[Order(24)]
public string FontID = "Default";
[ColorEdit4("Text Color")]
[Order(30)]
public PluginConfigColor TextColor = new(new Vector4(1f, 1f, 1f, 1f));
[ColorEdit4("Background Color")]
[Order(31)]
public PluginConfigColor BackgroundColor = new(new Vector4(0f, 0f, 0f, 0.5f));
[ColorEdit4("Divider Color")]
[Order(32)]
public PluginConfigColor DividerColor = new(new Vector4(1f, 1f, 1f, 0.6f));
[Checkbox("Show Duty Finder section when in queue")]
[Order(35)]
public bool ShowDutyFinderSection = true;
[ColorEdit4("Duty Finder title color")]
[Order(36, collapseWith = nameof(ShowDutyFinderSection))]
public PluginConfigColor DutyFinderTitleColor = new(new Vector4(1f, 0.85f, 0.2f, 1f));
[ColorEdit4("Duty Finder detail color")]
[Order(37, collapseWith = nameof(ShowDutyFinderSection))]
public PluginConfigColor DutyFinderDetailColor = new(new Vector4(0.4f, 0.75f, 1f, 1f));
[NestedConfig("Visibility", 70)]
public VisibilityConfig VisibilityConfig = new();
public DutyListScenarioConfig()
{
Size = new Vector2(320, 200);
}
public new static DutyListScenarioConfig DefaultConfig()
{
var config = new DutyListScenarioConfig
{
Position = new Vector2(ImGui.GetMainViewport().Size.X * 0.38f, -ImGui.GetMainViewport().Size.Y * 0.35f),
Size = new Vector2(320, 200),
Anchor = DrawAnchor.TopRight
};
return config;
}
}
}
@@ -0,0 +1,279 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using HSUI.Config;
using HSUI.Helpers;
using HSUI.Interface;
using System.Collections.Generic;
using System.Numerics;
namespace HSUI.Interface.GeneralElements
{
public class DutyListScenarioHud : DraggableHudElement, IHudElementWithVisibilityConfig
{
private DutyListScenarioConfig Config => (DutyListScenarioConfig)_config;
public VisibilityConfig VisibilityConfig => Config.VisibilityConfig;
public DutyListScenarioHud(DutyListScenarioConfig config, string displayName)
: base(config, displayName) { }
protected override (List<Vector2>, List<Vector2>) ChildrenPositionsAndSizes()
{
return (new List<Vector2> { Config.Position }, new List<Vector2> { Config.Size });
}
/// <summary>Screen-space bounds for visibility "hide unless hovered" must match the draw position exactly.</summary>
public override (Vector2 min, Vector2 max) GetScreenBounds(Vector2 origin)
{
Vector2 topLeft = Utils.GetAnchoredPosition(origin + ParentPos() + Config.Position, Config.Size, Config.Anchor);
return (topLeft, topLeft + Config.Size);
}
public override void DrawChildren(Vector2 origin)
{
if (!Config.Enabled)
return;
var dutyListEntries = DutyListScenarioHelper.GetDutyListEntries();
var scenarioEntries = DutyListScenarioHelper.GetScenarioGuideEntries();
var dutyFinderInfo = DutyListScenarioHelper.GetDutyFinderQueueInfo();
var dutyInfo = DutyListScenarioHelper.GetDutyInfo();
bool hasDutyList = dutyListEntries.Count > 0;
bool hasScenario = scenarioEntries.Count > 0;
// Show queue info when in queue; show duty info when in duty (not in queue). Same section, different content.
bool hasDutyFinder = Config.ShowDutyFinderSection && dutyFinderInfo != null;
bool hasDutyInfo = Config.ShowDutyFinderSection && dutyInfo != null;
if (!hasDutyList && !hasScenario && !hasDutyFinder && !hasDutyInfo)
return;
Vector2 basePos = Utils.GetAnchoredPosition(origin + Config.Position, Config.Size, Config.Anchor);
Vector2 iconSize = new Vector2(Config.IconSize, Config.IconSize);
float lineHeight = Config.IconSize + 4;
float padding = 6;
float contentWidth = Config.Size.X - padding * 2;
AddDrawAction(Config.StrataLevel, () =>
{
DrawHelper.DrawInWindow(ID, basePos, Config.Size, true, (drawList) =>
{
drawList.AddRectFilled(basePos, basePos + Config.Size, Config.BackgroundColor.Base, 4);
drawList.PushClipRect(basePos, basePos + Config.Size, true);
float y = basePos.Y + padding;
uint textColor = Config.TextColor.Base;
if (Config.FontID != "Default" && FontsManager.Instance != null)
{
using (FontsManager.Instance.PushFont(Config.FontID))
{
ImGui.SetWindowFontScale(Config.FontScale);
DrawContent(drawList, ref y, basePos.X + padding, contentWidth, lineHeight, iconSize, textColor, hasDutyList, hasScenario, hasDutyFinder, hasDutyInfo, dutyListEntries, scenarioEntries, dutyFinderInfo, dutyInfo);
ImGui.SetWindowFontScale(1);
}
}
else
{
ImGui.PushFont(UiBuilder.DefaultFont);
ImGui.SetWindowFontScale(Config.FontScale);
DrawContent(drawList, ref y, basePos.X + padding, contentWidth, lineHeight, iconSize, textColor, hasDutyList, hasScenario, hasDutyFinder, hasDutyInfo, dutyListEntries, scenarioEntries, dutyFinderInfo, dutyInfo);
ImGui.SetWindowFontScale(1);
ImGui.PopFont();
}
drawList.PopClipRect();
});
});
}
private static string TruncateText(string text, float maxWidth)
{
if (ImGui.CalcTextSize(text).X <= maxWidth)
return text;
string suffix = "...";
while (text.Length > 1 && ImGui.CalcTextSize(text + suffix).X > maxWidth)
text = text[..^1];
return text + suffix;
}
private void DrawContent(
ImDrawListPtr drawList,
ref float y,
float x,
float contentWidth,
float lineHeight,
Vector2 iconSize,
uint textColor,
bool hasDutyList,
bool hasScenario,
bool hasDutyFinder,
bool hasDutyInfo,
List<DutyListScenarioHelper.DutyListEntry> dutyListEntries,
List<DutyListScenarioHelper.ScenarioGuideEntry> scenarioEntries,
DutyListScenarioHelper.DutyFinderQueueInfo? dutyFinderInfo,
DutyListScenarioHelper.DutyInfo? dutyInfo)
{
const uint TankIconId = 62581;
const uint HealerIconId = 62582;
const uint DPSIconId = 62583;
if (hasDutyInfo && dutyInfo != null)
{
uint titleColor = Config.DutyFinderTitleColor.Base;
uint detailColor = Config.DutyFinderDetailColor.Base;
drawList.AddText(new Vector2(x, y), titleColor, "Duty Information");
y += lineHeight;
string dutyText = TruncateText(dutyInfo.DutyName, contentWidth);
drawList.AddText(new Vector2(x, y), detailColor, dutyText);
y += lineHeight;
if (!string.IsNullOrEmpty(dutyInfo.TimerText))
{
drawList.AddText(new Vector2(x, y), detailColor, TruncateText(dutyInfo.TimerText, contentWidth));
y += lineHeight;
}
foreach (var obj in dutyInfo.Objectives)
{
string line = string.IsNullOrWhiteSpace(obj.Text) ? "???" : obj.Text;
if (!string.IsNullOrWhiteSpace(obj.Progress))
line += ": " + obj.Progress;
drawList.AddText(new Vector2(x, y), detailColor, TruncateText(line, contentWidth));
y += lineHeight;
}
y += 4;
if (hasScenario || hasDutyList)
{
float divY = y + 2;
drawList.AddRectFilled(new Vector2(x, divY), new Vector2(x + contentWidth, divY + 2f), Config.DividerColor.Base);
y += 8;
}
}
else if (hasDutyFinder && dutyFinderInfo != null)
{
uint titleColor = Config.DutyFinderTitleColor.Base;
uint detailColor = Config.DutyFinderDetailColor.Base;
drawList.AddText(new Vector2(x, y), titleColor, "Duty Finder");
y += lineHeight;
string dutyText = TruncateText(dutyFinderInfo.DutyName, contentWidth);
drawList.AddText(new Vector2(x, y), detailColor, dutyText);
y += lineHeight;
string statusText = TruncateText(dutyFinderInfo.StatusText, contentWidth);
drawList.AddText(new Vector2(x, y), detailColor, statusText);
y += lineHeight;
float roleX = x;
roleX += 4;
DrawHelper.DrawIcon(TankIconId, new Vector2(roleX, y), new Vector2(iconSize.X * 0.8f, iconSize.Y * 0.8f), false, detailColor, drawList);
roleX += iconSize.X * 0.8f + 2;
drawList.AddText(new Vector2(roleX, y), detailColor, $":{dutyFinderInfo.TanksFound}/{dutyFinderInfo.TanksNeeded}");
roleX += ImGui.CalcTextSize($":{dutyFinderInfo.TanksFound}/{dutyFinderInfo.TanksNeeded}").X + 6;
DrawHelper.DrawIcon(HealerIconId, new Vector2(roleX, y), new Vector2(iconSize.X * 0.8f, iconSize.Y * 0.8f), false, detailColor, drawList);
roleX += iconSize.X * 0.8f + 2;
drawList.AddText(new Vector2(roleX, y), detailColor, $":{dutyFinderInfo.HealersFound}/{dutyFinderInfo.HealersNeeded}");
roleX += ImGui.CalcTextSize($":{dutyFinderInfo.HealersFound}/{dutyFinderInfo.HealersNeeded}").X + 6;
DrawHelper.DrawIcon(DPSIconId, new Vector2(roleX, y), new Vector2(iconSize.X * 0.8f, iconSize.Y * 0.8f), false, detailColor, drawList);
roleX += iconSize.X * 0.8f + 2;
drawList.AddText(new Vector2(roleX, y), detailColor, $":{dutyFinderInfo.DPSFound}/{dutyFinderInfo.DPSNeeded}");
y += lineHeight;
string elapsedStr = $"{(int)dutyFinderInfo.TimeElapsed.TotalMinutes}:{dutyFinderInfo.TimeElapsed.Seconds:D2}";
string timeLine = "Time Elapsed: " + elapsedStr;
if (!string.IsNullOrEmpty(dutyFinderInfo.AverageWaitText))
timeLine += " / Average Wait Time: " + dutyFinderInfo.AverageWaitText;
timeLine = TruncateText(timeLine, contentWidth);
drawList.AddText(new Vector2(x, y), detailColor, timeLine);
y += lineHeight + 4;
if (hasScenario || hasDutyList)
{
float divY = y + 2;
drawList.AddRectFilled(new Vector2(x, divY), new Vector2(x + contentWidth, divY + 2f), Config.DividerColor.Base);
y += 8;
}
}
if (hasScenario)
{
foreach (var entry in scenarioEntries)
{
float lineX = x;
if (Config.ShowQuestIcons && entry.IconId != 0)
{
DrawHelper.DrawIcon(entry.IconId, new Vector2(lineX, y), iconSize, false, textColor, drawList);
lineX += iconSize.X + 4;
}
string text = TruncateText($"{entry.Label}: Lv. {entry.Level} {entry.QuestName}", contentWidth - (lineX - x));
var textSize = ImGui.CalcTextSize(text);
drawList.AddText(new Vector2(lineX, y), textColor, text);
// Use full line height for hit area so the second line (Job) doesn't get covered by the first (MSQ)
if (ImGui.IsMouseHoveringRect(new Vector2(lineX, y), new Vector2(x + contentWidth, y + lineHeight))
&& ImGui.IsMouseClicked(ImGuiMouseButton.Left))
{
DutyListScenarioHelper.OpenMapForQuestIssuer(entry.QuestRowId);
}
y += lineHeight;
}
if (hasDutyList)
{
float divY = y + 2;
drawList.AddRectFilled(new Vector2(x, divY), new Vector2(x + contentWidth, divY + 2f), Config.DividerColor.Base);
y += 8;
}
}
if (hasDutyList)
{
uint objectiveColor = ImGui.ColorConvertFloat4ToU32(new Vector4(0.78f, 0.78f, 0.78f, 1f));
foreach (var entry in dutyListEntries)
{
float lineX = x;
if (Config.ShowQuestIcons && entry.IconId != 0)
{
DrawHelper.DrawIcon(entry.IconId, new Vector2(lineX, y), iconSize, false, textColor, drawList);
lineX += iconSize.X + 4;
}
string nameText = TruncateText(entry.QuestName, contentWidth - (lineX - x));
var nameSize = ImGui.CalcTextSize(nameText);
drawList.AddText(new Vector2(lineX, y), textColor, nameText);
if (entry.QuestId != 0
&& ImGui.IsMouseHoveringRect(new Vector2(lineX, y), new Vector2(lineX + nameSize.X, y + nameSize.Y))
&& ImGui.IsMouseClicked(ImGuiMouseButton.Left))
{
DutyListScenarioHelper.OpenJournalForQuest(entry.QuestId);
}
y += lineHeight;
if (!string.IsNullOrWhiteSpace(entry.ObjectiveText))
{
float objX = lineX + 8;
string objText = TruncateText("• " + entry.ObjectiveText, contentWidth - (objX - x));
var objSize = ImGui.CalcTextSize(objText);
drawList.AddText(new Vector2(objX, y), objectiveColor, objText);
if (entry.QuestId != 0
&& ImGui.IsMouseHoveringRect(new Vector2(objX, y), new Vector2(objX + objSize.X, y + objSize.Y))
&& ImGui.IsMouseClicked(ImGuiMouseButton.Left))
{
DutyListScenarioHelper.OpenMapForQuestObjective(entry.QuestId, entry.Sequence);
}
y += lineHeight;
}
}
}
}
}
}
+101
View File
@@ -30,6 +30,8 @@ namespace HSUI.Interface
private bool _hidingCastBar = false;
private Vector2 _pullTimerPos = Vector2.Zero;
private bool _hidingPullTimer = false;
private bool _hidingDutyListOffScreen = false;
private bool _toDoListVisibleBeforeHide = true;
public HudHelper()
{
@@ -63,6 +65,7 @@ namespace HSUI.Interface
SetGameHudElementsHidden(Array.Empty<uint>(), true);
UpdateDefaultCastBar(true);
UpdateDefaultPulltimer(true);
UpdateDefaultDutyList(true);
UpdateDefaultNameplates(true);
UpdateJobGauges(true);
}
@@ -107,6 +110,7 @@ namespace HSUI.Interface
UpdateJobGauges();
UpdateDefaultHudElementsHidden();
UpdateDefaultCastBar();
UpdateDefaultDutyList();
UpdateDefaultNameplates();
}
catch (Exception ex)
@@ -253,6 +257,13 @@ namespace HSUI.Interface
if (enemyList?.Enabled == true)
AddElements(ElementKind.EnemyList);
var dutyListScenario = ConfigurationManager.Instance?.GetConfigObject<DutyListScenarioConfig>();
if (dutyListScenario?.Enabled == true)
{
// Only hide Scenario Tree via layout. Keep ToDoList in layout so it stays created/updated; we move it off-screen in UpdateDefaultDutyList so we can read objective text.
AddElements(ElementKind.ScenarioTree);
}
// NamePlate is not in HUD Layout config; use UpdateDefaultNameplates (IsVisible) for that
if (IsCurrentJobHudEnabled())
{
@@ -293,6 +304,8 @@ namespace HSUI.Interface
/// <summary>Visibility (ByteValue2) of game HUD elements before we hid them. Restored on disable/unload.</summary>
private readonly Dictionary<uint, byte> _gameHudVisibilityBeforeHide = new();
/// <summary>When we force-show ToDoList for off-screen text reading, save its ByteValue2 here to restore on disable.</summary>
private readonly Dictionary<uint, byte> _gameHudForceShowBeforeRestore = new();
private unsafe void SetGameHudElementsHidden(uint[] hashesToHide, bool forceRestore)
{
@@ -316,6 +329,17 @@ namespace HSUI.Interface
hasChanges = true;
}
_gameHudVisibilityBeforeHide.Clear();
foreach (ref var entry in entries)
{
if (!_gameHudForceShowBeforeRestore.TryGetValue(entry.AddonNameHash, out byte saved))
continue;
if (entry.ByteValue2 == saved)
continue;
entry.ByteValue2 = saved;
hasChanges = true;
}
_gameHudForceShowBeforeRestore.Clear();
}
else
{
@@ -330,6 +354,44 @@ namespace HSUI.Interface
entry.ByteValue2 = 0x0;
hasChanges = true;
}
// When Duty List/Scenario is enabled, force-show the Duty List (ToDoList) so the addon is created and we can move it off-screen for text reading
var dutyListScenario = ConfigurationManager.Instance?.GetConfigObject<DutyListScenarioConfig>();
if (dutyListScenario?.Enabled == true)
{
uint toDoListHash = (uint)ElementKind.ToDoList;
foreach (ref var entry in entries)
{
if (entry.AddonNameHash != toDoListHash)
continue;
if (!_gameHudForceShowBeforeRestore.ContainsKey(entry.AddonNameHash))
_gameHudForceShowBeforeRestore[entry.AddonNameHash] = entry.ByteValue2;
if (entry.ByteValue2 != 0x0)
continue;
entry.ByteValue2 = 0x1;
hasChanges = true;
break;
}
}
else
{
uint toDoListHash = (uint)ElementKind.ToDoList;
if (_gameHudForceShowBeforeRestore.TryGetValue(toDoListHash, out byte saved))
{
foreach (ref var entry in entries)
{
if (entry.AddonNameHash != toDoListHash)
continue;
if (entry.ByteValue2 != saved)
{
entry.ByteValue2 = saved;
hasChanges = true;
}
_gameHudForceShowBeforeRestore.Remove(toDoListHash);
break;
}
}
}
}
if (hasChanges)
@@ -418,6 +480,45 @@ namespace HSUI.Interface
return;
}
/// <summary>When Duty List/Scenario is enabled, hide the game's Duty List addon with IsVisible=false so it stays at layout position and remains populated for objective text; restore on disable.</summary>
private unsafe void UpdateDefaultDutyList(bool forceRestore = false)
{
var dutyListScenario = ConfigurationManager.Instance?.GetConfigObject<DutyListScenarioConfig>();
bool shouldHide = !forceRestore && (dutyListScenario?.Enabled ?? false);
if (shouldHide && !_hidingDutyListOffScreen)
{
Plugin.AddonLifecycle.RegisterListener(AddonEvent.PreDraw, "_ToDoList", (addonEvent, args) =>
{
AtkUnitBase* addon = (AtkUnitBase*)args.Addon.Address;
if (addon == null) return;
addon->IsVisible = false;
});
var addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("_ToDoList", 1).Address;
if (addon == null) addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("_ToDoList", 0).Address;
if (addon != null)
{
_toDoListVisibleBeforeHide = addon->IsVisible;
addon->IsVisible = false;
}
_hidingDutyListOffScreen = true;
}
else if ((forceRestore || !shouldHide) && _hidingDutyListOffScreen)
{
Plugin.AddonLifecycle.UnregisterListener(AddonEvent.PreDraw, "_ToDoList");
var addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("_ToDoList", 1).Address;
if (addon == null) addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("_ToDoList", 0).Address;
if (addon != null)
addon->IsVisible = _toDoListVisibleBeforeHide;
_toDoListVisibleBeforeHide = true;
_hidingDutyListOffScreen = false;
}
}
private static readonly Dictionary<uint, Type> _jobIdToConfigType = new()
{
[JobIDs.PLD] = typeof(PaladinConfig), [JobIDs.WAR] = typeof(WarriorConfig),
+4
View File
@@ -448,6 +448,10 @@ namespace HSUI.Interface
var partyCooldownsHud = new PartyCooldownsHud(partyCooldownsConfig, "Party Cooldowns");
_hudElements.Add(partyCooldownsConfig, partyCooldownsHud);
_hudElementsWithPreview.Add(partyCooldownsHud);
var dutyListScenarioConfig = ConfigurationManager.Instance.GetConfigObject<DutyListScenarioConfig>();
var dutyListScenarioHud = new DutyListScenarioHud(dutyListScenarioConfig, "Duty List & Scenario Guide");
_hudElements.Add(dutyListScenarioConfig, dutyListScenarioHud);
}
public void Draw(uint jobId)
+8
View File
@@ -1,4 +1,12 @@
# 1.0.8.22
- **Duty List & Scenario Guide**: Fix "Hide unless hovered" visibility option — override GetScreenBounds() so hover hit-test matches drawn panel.
- **Tooltips**: Job gauge/resource costs (Soul Gauge, Beast Gauge, Lily, Kenki, etc.) now show as section with label color. Fix tooltip break when stats and description share first block.
# 1.0.8.21
- **Duty List & Scenario Guide**: Duty Information when inside a duty (dungeon/trial/raid) — shows duty name, elapsed timer, and objectives. Replaces queue info with in-duty info when BoundByDuty. Objective progress shown as "X out of Y step(s)" with checkmark when completed (replaces raw 0/1:0). Average wait time from queue status messages (static format preferred); multiple queued duties supported; redundant "Average Wait Time" label fixed.
# 1.0.8.20
- **Duty List & Scenario Guide**: New combined HUD element replacing the game's Duty List and Scenario Guide. Shows active quests and levequests with quest icons, plus Main Scenario and Job Quest hints. Configurable position, size, font, colors, and visibility.
- **Hotbars**: Fixed cooldown overlays showing through game UI (e.g. dialogue box) when "Show HUD during dialogue and interaction" is enabled. Hotbar cooldown timers and numbers are now skipped for slots that overlap dialogue, select, or journal addons.
# 1.0.8.19