Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a61e11e37 | |||
| 27e743c80f | |||
| 2822f730f5 | |||
| fa0210a986 | |||
| 3618fc5121 | |||
| 1c25918c22 | |||
| c2e0b1be62 |
@@ -708,6 +708,7 @@ namespace HSUI.Config
|
|||||||
typeof(PullTimerConfig),
|
typeof(PullTimerConfig),
|
||||||
typeof(LimitBreakConfig),
|
typeof(LimitBreakConfig),
|
||||||
typeof(MPTickerConfig),
|
typeof(MPTickerConfig),
|
||||||
|
typeof(DutyListScenarioConfig),
|
||||||
|
|
||||||
// Colors
|
// Colors
|
||||||
typeof(TanksColorConfig),
|
typeof(TanksColorConfig),
|
||||||
@@ -724,7 +725,6 @@ namespace HSUI.Config
|
|||||||
|
|
||||||
// Visibility
|
// Visibility
|
||||||
typeof(GlobalVisibilityConfig),
|
typeof(GlobalVisibilityConfig),
|
||||||
typeof(HotbarsVisibilityConfig),
|
|
||||||
|
|
||||||
// Misc
|
// Misc
|
||||||
typeof(HUDOptionsConfig),
|
typeof(HUDOptionsConfig),
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ public enum ElementKind : uint
|
|||||||
EnemyList = 0xB8BD6685, // _EnemyList_a
|
EnemyList = 0xB8BD6685, // _EnemyList_a
|
||||||
|
|
||||||
// Misc HUD
|
// Misc HUD
|
||||||
|
ToDoList = 0xA29100D2, // _ToDoList_a (Duty List)
|
||||||
|
ScenarioTree = 0x88EE6357, // ScenarioTree_a (Scenario Guide)
|
||||||
ExperienceBar = 0x21E53CCE, // _Exp_a
|
ExperienceBar = 0x21E53CCE, // _Exp_a
|
||||||
LimitGauge = 0xC79F450A, // _LimitBreak_a
|
LimitGauge = 0xC79F450A, // _LimitBreak_a
|
||||||
StatusEffects = 0x4A569616, // _Status_a
|
StatusEffects = 0x4A569616, // _Status_a
|
||||||
|
|||||||
+4
-7
@@ -9,9 +9,9 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AssemblyName>HSUI</AssemblyName>
|
<AssemblyName>HSUI</AssemblyName>
|
||||||
<AssemblyVersion>1.0.8.16</AssemblyVersion>
|
<AssemblyVersion>1.0.8.21</AssemblyVersion>
|
||||||
<FileVersion>1.0.8.16</FileVersion>
|
<FileVersion>1.0.8.21</FileVersion>
|
||||||
<InformationalVersion>1.0.8.16</InformationalVersion>
|
<InformationalVersion>1.0.8.21</InformationalVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
@@ -43,10 +43,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="KamiToolKit">
|
<ProjectReference Include="..\repos\KamiToolKit-master\KamiToolKit.csproj" />
|
||||||
<HintPath>$(ProjectDir)lib\KamiToolKit.dll</HintPath>
|
|
||||||
<Private>true</Private>
|
|
||||||
</Reference>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"Author": "Knack117",
|
"Author": "Knack117",
|
||||||
"Name": "HSUI",
|
"Name": "HSUI",
|
||||||
"InternalName": "HSUI",
|
"InternalName": "HSUI",
|
||||||
"AssemblyVersion": "1.0.8.16",
|
"AssemblyVersion": "1.0.8.21",
|
||||||
"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.",
|
"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",
|
"ApplicableVersion": "any",
|
||||||
"RepoUrl": "https://github.com/Knack117/HSUI",
|
"RepoUrl": "https://github.com/Knack117/HSUI",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Dalamud.Game.ClientState.Conditions;
|
||||||
using HSUI.Config;
|
using HSUI.Config;
|
||||||
using HSUI.Interface.GeneralElements;
|
using HSUI.Interface.GeneralElements;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
@@ -335,6 +336,56 @@ namespace HSUI.Helpers
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true when the given area overlaps a game addon (e.g. dialogue box) and should not be drawn on top of it.
|
||||||
|
/// Used by hotbars when "Show HUD during dialogue" is on, to avoid cooldown overlays bleeding through dialogue.
|
||||||
|
/// Uses full clip rects when Window Clipping is enabled, otherwise a minimal dialogue-addon check.
|
||||||
|
/// </summary>
|
||||||
|
public bool SlotOverlapsGameAddon(Vector2 pos, Vector2 size)
|
||||||
|
{
|
||||||
|
// When Window Clipping is enabled, use full clip rects (all game addons)
|
||||||
|
ClipRect? clipRect = GetClipRectForArea(pos, size);
|
||||||
|
if (clipRect.HasValue) { return true; }
|
||||||
|
|
||||||
|
// When "Show HUD during dialogue" is on and we're in dialogue, check dialogue addons
|
||||||
|
// (works even when full Window Clipping is disabled)
|
||||||
|
var hudOptions = ConfigurationManager.Instance?.GetConfigObject<HUDOptionsConfig>();
|
||||||
|
if (hudOptions?.ShowHudDuringDialogue != true) { return false; }
|
||||||
|
if (!Plugin.Condition[ConditionFlag.OccupiedInQuestEvent] && !Plugin.Condition[ConditionFlag.OccupiedInEvent])
|
||||||
|
{ return false; }
|
||||||
|
|
||||||
|
return GetClipRectForDialogueAddon(pos, size).HasValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly string[] _dialogueAddonNames = { "Talk", "SelectString", "SelectIconString", "Journal" };
|
||||||
|
|
||||||
|
private unsafe ClipRect? GetClipRectForDialogueAddon(Vector2 pos, Vector2 size)
|
||||||
|
{
|
||||||
|
ClipRect area = new ClipRect(pos, pos + size);
|
||||||
|
|
||||||
|
foreach (string addonName in _dialogueAddonNames)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName(addonName, 1).Address;
|
||||||
|
if (addon == null || addon->RootNode == null || !addon->IsVisible) { continue; }
|
||||||
|
|
||||||
|
float margin = 5 * addon->Scale;
|
||||||
|
Vector2 addonPos = new Vector2(addon->X + addon->RootNode->X * addon->Scale + margin,
|
||||||
|
addon->Y + addon->RootNode->Y * addon->Scale + margin);
|
||||||
|
Vector2 addonSize = new Vector2(
|
||||||
|
addon->RootNode->Width * addon->Scale - margin * 2,
|
||||||
|
addon->RootNode->Height * addon->Scale - margin * 2);
|
||||||
|
ClipRect addonRect = new ClipRect(addonPos, addonPos + addonSize);
|
||||||
|
|
||||||
|
if (addonRect.IntersectsWith(area)) { return addonRect; }
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public static ClipRect[] GetInvertedClipRects(ClipRect clipRect)
|
public static ClipRect[] GetInvertedClipRects(ClipRect clipRect)
|
||||||
{
|
{
|
||||||
float maxX = ImGui.GetMainViewport().Size.X;
|
float maxX = ImGui.GetMainViewport().Size.X;
|
||||||
|
|||||||
@@ -358,10 +358,13 @@ namespace HSUI.Helpers
|
|||||||
{
|
{
|
||||||
|
|
||||||
if (!ClipRectsHelper.Instance.Enabled || ClipRectsHelper.Instance.Mode == WindowClippingMode.Performance)
|
if (!ClipRectsHelper.Instance.Enabled || ClipRectsHelper.Instance.Mode == WindowClippingMode.Performance)
|
||||||
|
{
|
||||||
|
if (!needsInput && !needsWindow)
|
||||||
{
|
{
|
||||||
drawAction(ImGui.GetWindowDrawList());
|
drawAction(ImGui.GetWindowDrawList());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
windowFlags |= ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoBringToFrontOnFocus;
|
windowFlags |= ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoBringToFrontOnFocus;
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+37
-12
@@ -463,8 +463,9 @@ namespace HSUI.Helpers
|
|||||||
if (string.IsNullOrEmpty(description))
|
if (string.IsNullOrEmpty(description))
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
// Pattern: section labels to color (Additional Effect, Duration, X Effect, Maximum Charges, Blood Gauge Cost, Combo, roles)
|
// Pattern: section labels to color (Additional Effect, Duration, X Effect, Maximum Charges, job gauge costs, 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);
|
// 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
|
// Pattern: "Grants X X Effect:" - captures status name X
|
||||||
var grantsRegex = new Regex(@"Grants\s+(.+?)\s+\1\s+Effect:", RegexOptions.Singleline);
|
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 secondaryColor = config.SecondaryLabelColor.Vector;
|
||||||
|
|
||||||
var blocks = body.Split(new[] { "\n\n" }, StringSplitOptions.None);
|
var blocks = body.Split(new[] { "\n\n" }, StringSplitOptions.None);
|
||||||
int descStart = 0;
|
|
||||||
bool hasStats = false;
|
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++)
|
for (int i = 0; i < blocks.Length; i++)
|
||||||
{
|
{
|
||||||
string block = blocks[i].Trim();
|
string block = blocks[i].Trim();
|
||||||
var lines = block.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
var lines = block.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
bool blockIsStats = false;
|
List<string> descLinesInBlock = new List<string>();
|
||||||
foreach (string line in lines)
|
foreach (string line in lines)
|
||||||
{
|
{
|
||||||
string part = line.Trim();
|
string part = line.Trim();
|
||||||
|
if (inDescription)
|
||||||
|
{
|
||||||
|
descLinesInBlock.Add(part);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (part.StartsWith("Potency:", StringComparison.OrdinalIgnoreCase))
|
if (part.StartsWith("Potency:", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
if (hasStats) result.Add(new TooltipSegment("\n", textColor));
|
if (hasStats) result.Add(new TooltipSegment("\n", textColor));
|
||||||
result.Add(new TooltipSegment("Potency:", secondaryColor));
|
result.Add(new TooltipSegment("Potency:", secondaryColor));
|
||||||
result.Add(new TooltipSegment(" " + part.Substring(8).TrimStart(), textColor));
|
result.Add(new TooltipSegment(" " + part.Substring(8).TrimStart(), textColor));
|
||||||
hasStats = true;
|
hasStats = true;
|
||||||
blockIsStats = true;
|
|
||||||
}
|
}
|
||||||
else if (part.Contains("Cast:") || part.Contains("Recast:"))
|
else if (part.Contains("Cast:") || part.Contains("Recast:"))
|
||||||
{
|
{
|
||||||
@@ -605,19 +614,35 @@ namespace HSUI.Helpers
|
|||||||
var statsSegs = FormatActionStats(part, textColor, secondaryColor);
|
var statsSegs = FormatActionStats(part, textColor, secondaryColor);
|
||||||
result.AddRange(statsSegs);
|
result.AddRange(statsSegs);
|
||||||
hasStats = true;
|
hasStats = true;
|
||||||
blockIsStats = true;
|
|
||||||
}
|
}
|
||||||
}
|
else if (gaugeCostLineRegex.IsMatch(part))
|
||||||
if (!blockIsStats)
|
|
||||||
{
|
{
|
||||||
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;
|
break;
|
||||||
}
|
}
|
||||||
descStart = i + 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Description: exactly one newline after stats, then description with compact section spacing
|
// 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 (!string.IsNullOrEmpty(desc))
|
||||||
{
|
{
|
||||||
if (hasStats) result.Add(new TooltipSegment("\n", textColor));
|
if (hasStats) result.Add(new TooltipSegment("\n", textColor));
|
||||||
|
|||||||
@@ -256,6 +256,13 @@ namespace HSUI.Interface
|
|||||||
{
|
{
|
||||||
return (new List<Vector2>(), new List<Vector2>());
|
return (new List<Vector2>(), new List<Vector2>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns the screen-space bounding box of the element for hover detection (e.g. visibility "hide unless hovered").</summary>
|
||||||
|
public virtual (Vector2 min, Vector2 max) GetScreenBounds(Vector2 origin)
|
||||||
|
{
|
||||||
|
var basePos = origin + ParentPos();
|
||||||
|
return (basePos + MinPos, basePos + MaxPos);
|
||||||
|
}
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,19 +72,9 @@ namespace HSUI.Interface.GeneralElements
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Visibility for HSUI Action Bars is driven by Visibility → Hotbars (Hotbar 1–10), not the per-element config.
|
/// Visibility for HSUI Action Bars is configured per hotbar in each Hotbar 1–10 menu.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public VisibilityConfig VisibilityConfig => GetHotbarVisibilityConfig();
|
public VisibilityConfig VisibilityConfig => Config.VisibilityConfig;
|
||||||
|
|
||||||
private VisibilityConfig GetHotbarVisibilityConfig()
|
|
||||||
{
|
|
||||||
var hotbars = ConfigurationManager.Instance?.GetConfigObject<HotbarsVisibilityConfig>();
|
|
||||||
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 IGameObject? Actor { get; set; }
|
||||||
|
|
||||||
@@ -186,6 +176,10 @@ namespace HSUI.Interface.GeneralElements
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showCd && slot.CooldownPercent > 0 && _pendingSlotIconIndex != i)
|
if (showCd && slot.CooldownPercent > 0 && _pendingSlotIconIndex != i)
|
||||||
|
{
|
||||||
|
// Skip cooldown overlay when slot overlaps game UI (e.g. dialogue box)
|
||||||
|
// to avoid hotbar cooldowns showing through during "Show HUD during dialogue"
|
||||||
|
if (!ClipRectsHelper.Instance.SlotOverlapsGameAddon(pos, size))
|
||||||
{
|
{
|
||||||
float total = 100f;
|
float total = 100f;
|
||||||
float elapsed = total - slot.CooldownPercent;
|
float elapsed = total - slot.CooldownPercent;
|
||||||
@@ -198,6 +192,7 @@ namespace HSUI.Interface.GeneralElements
|
|||||||
DrawHelper.DrawOutlinedText(cdText, textPos, 0xFFFFFFFF, 0xFF000000, drawList, 1);
|
DrawHelper.DrawOutlinedText(cdText, textPos, 0xFFFFFFFF, 0xFF000000, drawList, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (Config.ShowKeypressFlash)
|
if (Config.ShowKeypressFlash)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -316,6 +316,10 @@ namespace HSUI.Interface.GeneralElements
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showCd && slot.CooldownPercent > 0 && _pendingSlotIconIndex != i)
|
if (showCd && slot.CooldownPercent > 0 && _pendingSlotIconIndex != i)
|
||||||
|
{
|
||||||
|
// Skip cooldown overlay when slot overlaps game UI (e.g. dialogue box)
|
||||||
|
// to avoid hotbar cooldowns showing through during "Show HUD during dialogue"
|
||||||
|
if (!ClipRectsHelper.Instance.SlotOverlapsGameAddon(pos, size))
|
||||||
{
|
{
|
||||||
float total = 100f;
|
float total = 100f;
|
||||||
float elapsed = total - slot.CooldownPercent;
|
float elapsed = total - slot.CooldownPercent;
|
||||||
@@ -328,6 +332,7 @@ namespace HSUI.Interface.GeneralElements
|
|||||||
DrawHelper.DrawOutlinedText(cdText, textPos, 0xFFFFFFFF, 0xFF000000, drawList, 1);
|
DrawHelper.DrawOutlinedText(cdText, textPos, 0xFFFFFFFF, 0xFF000000, drawList, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (Config.ShowKeypressFlash)
|
if (Config.ShowKeypressFlash)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -153,23 +153,9 @@ namespace HSUI.Interface.GeneralElements
|
|||||||
{
|
{
|
||||||
if (bar == null) continue;
|
if (bar == null) continue;
|
||||||
bar.Enabled = true;
|
bar.Enabled = true;
|
||||||
|
bar.VisibilityConfig.CopyFrom(new VisibilityConfig());
|
||||||
HotbarBarConfig.ApplyDefaults(bar, idx);
|
HotbarBarConfig.ApplyDefaults(bar, idx);
|
||||||
}
|
}
|
||||||
var vis = cfg.GetConfigObject<HotbarsVisibilityConfig>();
|
|
||||||
if (vis != null)
|
|
||||||
{
|
|
||||||
var defaults = new VisibilityConfig();
|
|
||||||
vis.HotbarConfig1.CopyFrom(defaults);
|
|
||||||
vis.HotbarConfig2.CopyFrom(defaults);
|
|
||||||
vis.HotbarConfig3.CopyFrom(defaults);
|
|
||||||
vis.HotbarConfig4.CopyFrom(defaults);
|
|
||||||
vis.HotbarConfig5.CopyFrom(defaults);
|
|
||||||
vis.HotbarConfig6.CopyFrom(defaults);
|
|
||||||
vis.HotbarConfig7.CopyFrom(defaults);
|
|
||||||
vis.HotbarConfig8.CopyFrom(defaults);
|
|
||||||
vis.HotbarConfig9.CopyFrom(defaults);
|
|
||||||
vis.HotbarConfig10.CopyFrom(defaults);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
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<VisibilityConfig> _configs;
|
|
||||||
public List<VisibilityConfig> GetHotbarConfigs() => _configs;
|
|
||||||
|
|
||||||
public HotbarsVisibilityConfig()
|
|
||||||
{
|
|
||||||
_configs = new List<VisibilityConfig>() {
|
|
||||||
HotbarConfig1,
|
|
||||||
HotbarConfig2,
|
|
||||||
HotbarConfig3,
|
|
||||||
HotbarConfig4,
|
|
||||||
HotbarConfig5,
|
|
||||||
HotbarConfig6,
|
|
||||||
HotbarConfig7,
|
|
||||||
HotbarConfig8,
|
|
||||||
HotbarConfig9,
|
|
||||||
HotbarConfig10
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Game.ClientState.Conditions;
|
using Dalamud.Game.ClientState.Conditions;
|
||||||
using Dalamud.Game.ClientState.Objects.Enums;
|
using Dalamud.Game.ClientState.Objects.Enums;
|
||||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||||
@@ -6,12 +7,17 @@ using HSUI.Config.Attributes;
|
|||||||
using HSUI.Interface.GeneralElements;
|
using HSUI.Interface.GeneralElements;
|
||||||
using HSUI.Interface.Party;
|
using HSUI.Interface.Party;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
namespace HSUI.Interface
|
namespace HSUI.Interface
|
||||||
{
|
{
|
||||||
[Exportable(false)]
|
[Exportable(false)]
|
||||||
public class VisibilityConfig : PluginConfigObject
|
public class VisibilityConfig : PluginConfigObject
|
||||||
{
|
{
|
||||||
|
[Checkbox("Hide unless hovered", help = "When enabled, the element is hidden until the cursor hovers over it. Requires HUD to be locked.")]
|
||||||
|
[Order(4)]
|
||||||
|
public bool HideUnlessHovered = false;
|
||||||
|
|
||||||
[Checkbox("Hide outside of combat")]
|
[Checkbox("Hide outside of combat")]
|
||||||
[Order(5)]
|
[Order(5)]
|
||||||
public bool HideOutsideOfCombat = false;
|
public bool HideOutsideOfCombat = false;
|
||||||
@@ -96,6 +102,18 @@ namespace HSUI.Interface
|
|||||||
if (element != null && element.GetType() == typeof(PlayerCastbarHud)) { return true; }
|
if (element != null && element.GetType() == typeof(PlayerCastbarHud)) { return true; }
|
||||||
if (element != null && !element.GetConfig().Enabled) { return false; }
|
if (element != null && !element.GetConfig().Enabled) { return false; }
|
||||||
|
|
||||||
|
// Hide unless hovered: element is visible only when the cursor is over it
|
||||||
|
if (HideUnlessHovered && element is DraggableHudElement draggable)
|
||||||
|
{
|
||||||
|
Vector2 origin = ImGui.GetMainViewport().Size / 2f;
|
||||||
|
var hudOptions = ConfigurationManager.Instance?.GetConfigObject<HUDOptionsConfig>();
|
||||||
|
if (hudOptions != null && hudOptions.UseGlobalHudShift)
|
||||||
|
origin += hudOptions.HudOffset;
|
||||||
|
var (min, max) = draggable.GetScreenBounds(origin);
|
||||||
|
if (!ImGui.IsMouseHoveringRect(min, max))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Alliance frames only matter in alliance raids (duty). HideInDuty would hide them exactly when needed.
|
// Alliance frames only matter in alliance raids (duty). HideInDuty would hide them exactly when needed.
|
||||||
bool isAllianceFrames = element != null && element.GetType() == typeof(AllianceFramesHud);
|
bool isAllianceFrames = element != null && element.GetType() == typeof(AllianceFramesHud);
|
||||||
|
|
||||||
@@ -144,6 +162,7 @@ namespace HSUI.Interface
|
|||||||
{
|
{
|
||||||
Enabled = config.Enabled;
|
Enabled = config.Enabled;
|
||||||
|
|
||||||
|
HideUnlessHovered = config.HideUnlessHovered;
|
||||||
HideOutsideOfCombat = config.HideOutsideOfCombat;
|
HideOutsideOfCombat = config.HideOutsideOfCombat;
|
||||||
HideInCombat = config.HideInCombat;
|
HideInCombat = config.HideInCombat;
|
||||||
HideInGoldSaucer = config.HideInGoldSaucer;
|
HideInGoldSaucer = config.HideInGoldSaucer;
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ namespace HSUI.Interface
|
|||||||
private bool _hidingCastBar = false;
|
private bool _hidingCastBar = false;
|
||||||
private Vector2 _pullTimerPos = Vector2.Zero;
|
private Vector2 _pullTimerPos = Vector2.Zero;
|
||||||
private bool _hidingPullTimer = false;
|
private bool _hidingPullTimer = false;
|
||||||
|
private bool _hidingDutyListOffScreen = false;
|
||||||
|
private bool _toDoListVisibleBeforeHide = true;
|
||||||
|
|
||||||
public HudHelper()
|
public HudHelper()
|
||||||
{
|
{
|
||||||
@@ -63,6 +65,7 @@ namespace HSUI.Interface
|
|||||||
SetGameHudElementsHidden(Array.Empty<uint>(), true);
|
SetGameHudElementsHidden(Array.Empty<uint>(), true);
|
||||||
UpdateDefaultCastBar(true);
|
UpdateDefaultCastBar(true);
|
||||||
UpdateDefaultPulltimer(true);
|
UpdateDefaultPulltimer(true);
|
||||||
|
UpdateDefaultDutyList(true);
|
||||||
UpdateDefaultNameplates(true);
|
UpdateDefaultNameplates(true);
|
||||||
UpdateJobGauges(true);
|
UpdateJobGauges(true);
|
||||||
}
|
}
|
||||||
@@ -107,6 +110,7 @@ namespace HSUI.Interface
|
|||||||
UpdateJobGauges();
|
UpdateJobGauges();
|
||||||
UpdateDefaultHudElementsHidden();
|
UpdateDefaultHudElementsHidden();
|
||||||
UpdateDefaultCastBar();
|
UpdateDefaultCastBar();
|
||||||
|
UpdateDefaultDutyList();
|
||||||
UpdateDefaultNameplates();
|
UpdateDefaultNameplates();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -253,6 +257,13 @@ namespace HSUI.Interface
|
|||||||
if (enemyList?.Enabled == true)
|
if (enemyList?.Enabled == true)
|
||||||
AddElements(ElementKind.EnemyList);
|
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
|
// NamePlate is not in HUD Layout config; use UpdateDefaultNameplates (IsVisible) for that
|
||||||
if (IsCurrentJobHudEnabled())
|
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>
|
/// <summary>Visibility (ByteValue2) of game HUD elements before we hid them. Restored on disable/unload.</summary>
|
||||||
private readonly Dictionary<uint, byte> _gameHudVisibilityBeforeHide = new();
|
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)
|
private unsafe void SetGameHudElementsHidden(uint[] hashesToHide, bool forceRestore)
|
||||||
{
|
{
|
||||||
@@ -316,6 +329,17 @@ namespace HSUI.Interface
|
|||||||
hasChanges = true;
|
hasChanges = true;
|
||||||
}
|
}
|
||||||
_gameHudVisibilityBeforeHide.Clear();
|
_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
|
else
|
||||||
{
|
{
|
||||||
@@ -330,6 +354,44 @@ namespace HSUI.Interface
|
|||||||
entry.ByteValue2 = 0x0;
|
entry.ByteValue2 = 0x0;
|
||||||
hasChanges = true;
|
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)
|
if (hasChanges)
|
||||||
@@ -418,6 +480,45 @@ namespace HSUI.Interface
|
|||||||
return;
|
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()
|
private static readonly Dictionary<uint, Type> _jobIdToConfigType = new()
|
||||||
{
|
{
|
||||||
[JobIDs.PLD] = typeof(PaladinConfig), [JobIDs.WAR] = typeof(WarriorConfig),
|
[JobIDs.PLD] = typeof(PaladinConfig), [JobIDs.WAR] = typeof(WarriorConfig),
|
||||||
|
|||||||
@@ -448,6 +448,10 @@ namespace HSUI.Interface
|
|||||||
var partyCooldownsHud = new PartyCooldownsHud(partyCooldownsConfig, "Party Cooldowns");
|
var partyCooldownsHud = new PartyCooldownsHud(partyCooldownsConfig, "Party Cooldowns");
|
||||||
_hudElements.Add(partyCooldownsConfig, partyCooldownsHud);
|
_hudElements.Add(partyCooldownsConfig, partyCooldownsHud);
|
||||||
_hudElementsWithPreview.Add(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)
|
public void Draw(uint jobId)
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
# 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
|
||||||
|
- **Hotbars**: Visibility settings moved from Visibility → Hotbars to each Hotbar 1–10 menu. The Visibility → Hotbars tab has been removed.
|
||||||
|
|
||||||
|
# 1.0.8.18
|
||||||
|
- **Visibility**: "Hide unless hovered" — per-element option in Visibility to hide any UI element unless the cursor is over it (requires HUD locked).
|
||||||
|
|
||||||
|
# 1.0.8.17
|
||||||
|
- **Controller Hotbars**: Add 8 cross bars (L2/R2 style) separate from normal hotbars 1-8. Controller bar slot data stored in config (not game StandardHotbars) so layouts can differ per mode. Drag-and-drop on controller bars: from game, shift+drag rearrange, release outside to clear. Independent keybinds with modifier+trigger combinations (e.g. L2+South). Optional sync bar mode with game client (Character Config Mouse/Gamepad toggle). Clone/copy actions: normal hotbars ↔ controller bars. Restore controller bar layout button.
|
||||||
|
|
||||||
# 1.0.8.16
|
# 1.0.8.16
|
||||||
- **HUD Options**: "Show HUD during dialogue and interaction" — when enabled, hotbars and the rest of HSUI stay visible when talking to NPCs or interacting with objects (default on). When disabled, only the castbar is shown during dialogue after a short delay.
|
- **HUD Options**: "Show HUD during dialogue and interaction" — when enabled, hotbars and the rest of HSUI stay visible when talking to NPCs or interacting with objects (default on). When disabled, only the castbar is shown during dialogue after a short delay.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
[{"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":"1.0.8.16: Show HUD during dialogue and interaction (hotbars stay visible when talking to NPCs or interacting). 1.0.8.15: Tooltips game-style formatting (section labels, tail alignment, config). 1.0.8.14: Mouse GCD Indicator (ring around cursor). 1.0.8.13: Item tooltips now show. 1.0.8.12: Item/HQ icons now draw on hotbar. 1.0.8.11: Hotbar tooltip crash fix (Lumina GetRow). 1.0.8.10: Hotbar crash fix when dragging inventory items; Release builds strip PDBs. 1.0.8.9: Gearset persists on slot (fix CommandId 0 treated as empty). 1.0.8.8: Gearset drag-drop fix; allow first gearset (index 0) on bar. 1.0.8.7: Fixed Gearset/Job Gear Set icon clearing when first equipment slot changes. 1.0.8.6: Crafting action tooltips now show full description instead of action name. 1.0.8.4: Alliance Frames 1 and 2 fix in raids; crafting action tooltips; Hide in duty no longer hides alliance frames. 1.0.8.3: Fix left-click staying broken after disable. 1.0.8.2: Charge icons stay lit until all charges spent.","InternalName":"HSUI","AssemblyVersion":"1.0.8.16","RepoUrl":"http://brassnet.ddns.net:33983/KnackAtNite/HSUI","ApplicableVersion":"any","Tags":["UI","HUD","Unit Frames","Nameplates","Party Frames","Hotbars"],"CategoryTags":["UI"],"DalamudApiLevel":14,"IconUrl":"http://brassnet.ddns.net:33983/KnackAtNite/HSUI/raw/branch/main/Media/Images/icon.png","ImageUrls":[],"DownloadLinkInstall":"http://brassnet.ddns.net:33983/KnackAtNite/HSUI/releases/download/v1.0.8.16/latest.zip","IsHide":false,"IsTestingExclusive":false,"DownloadLinkTesting":"http://brassnet.ddns.net:33983/KnackAtNite/HSUI/releases/download/v1.0.8.16/latest.zip","DownloadLinkUpdate":"http://brassnet.ddns.net:33983/KnackAtNite/HSUI/releases/download/v1.0.8.16/latest.zip","LastUpdate":"1761609600"}]
|
[{"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":"1.0.8.20: Fixed cooldown overlays showing through game UI (dialogue box) when Show HUD during dialogue enabled. 1.0.8.19: Hotbar visibility moved to per-hotbar menus; removed Visibility Hotbars tab. 1.0.8.18: Visibility \"Hide unless hovered\" option. 1.0.8.17: Controller hotbars (cross layout, separate storage, sync with game). 1.0.8.16: Show HUD during dialogue and interaction. 1.0.8.15: Tooltips game-style formatting. 1.0.8.14: Mouse GCD Indicator. 1.0.8.13: Item tooltips now show. 1.0.8.12: Item/HQ icons now draw on hotbar. 1.0.8.11: Hotbar tooltip crash fix. 1.0.8.10: Hotbar crash fix when dragging inventory items. 1.0.8.9: Gearset persists on slot. 1.0.8.8: Gearset drag-drop fix. 1.0.8.7: Fixed Gearset icon clearing. 1.0.8.6: Crafting action tooltips full description. 1.0.8.4: Alliance Frames 1 and 2 fix; Hide in duty no longer hides alliance frames. 1.0.8.3: Fix left-click staying broken after disable. 1.0.8.2: Charge icons stay lit until all charges spent.","InternalName":"HSUI","AssemblyVersion":"1.0.8.20","RepoUrl":"http://brassnet.ddns.net:33983/KnackAtNite/HSUI","ApplicableVersion":"any","Tags":["UI","HUD","Unit Frames","Nameplates","Party Frames","Hotbars"],"CategoryTags":["UI"],"DalamudApiLevel":14,"IconUrl":"http://brassnet.ddns.net:33983/KnackAtNite/HSUI/raw/branch/main/Media/Images/icon.png","ImageUrls":[],"DownloadLinkInstall":"http://brassnet.ddns.net:33983/KnackAtNite/HSUI/releases/download/v1.0.8.20/latest.zip","IsHide":false,"IsTestingExclusive":false,"DownloadLinkTesting":"http://brassnet.ddns.net:33983/KnackAtNite/HSUI/releases/download/v1.0.8.20/latest.zip","DownloadLinkUpdate":"http://brassnet.ddns.net:33983/KnackAtNite/HSUI/releases/download/v1.0.8.20/latest.zip","LastUpdate":"1772378453"}]
|
||||||
|
|||||||
Reference in New Issue
Block a user