feat: begin replacing layout tab with editor

This commit is contained in:
Anna 2021-03-10 14:08:28 -05:00
parent cb727afc72
commit 6b71493f6b
18 changed files with 1040 additions and 351 deletions

View File

@ -9,14 +9,16 @@ namespace HUD_Manager.Configuration {
public class SavedLayout {
public Dictionary<ElementKind, Element> Elements { get; }
public Dictionary<string, Vector2<short>> Positions { get; private set; }
public Guid Parent { get; set; } = Guid.Empty;
public string Name { get; private set; }
public string Name { get; set; }
[JsonConstructor]
public SavedLayout(string name, Dictionary<ElementKind, Element> elements, Dictionary<string, Vector2<short>> positions) {
public SavedLayout(string name, Dictionary<ElementKind, Element> elements, Dictionary<string, Vector2<short>> positions, Guid parent) {
this.Name = name;
this.Elements = elements;
this.Positions = positions;
this.Parent = parent;
}
public SavedLayout(string name, Layout hud, Dictionary<string, Vector2<short>> positions) {
@ -29,11 +31,11 @@ namespace HUD_Manager.Configuration {
var elements = this.Elements.Values.ToList();
while (elements.Count < 81) {
elements.Add(new Element());
elements.Add(new Element(new RawElement()));
}
return new Layout {
elements = elements.ToArray(),
elements = elements.Select(elem => new RawElement(elem)).ToArray(),
};
}
}

4
HUD Manager/FodyWeavers.xml Executable file
View File

@ -0,0 +1,4 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Resourcer/>
<Costura/>
</Weavers>

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net48</TargetFramework>
<Version>2.0.0.1</Version>
<Version>2.0.0.2</Version>
<LangVersion>latest</LangVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
@ -34,6 +34,16 @@
</Reference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="DalamudPackager" Version="1.2.0" />
<PackageReference Include="Costura.Fody" Version="5.0.2" PrivateAssets="all"/>
<PackageReference Include="DalamudPackager" Version="1.2.0"/>
<PackageReference Include="Fody" Version="6.4.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Resourcer.Fody" Version="1.8.0" PrivateAssets="all"/>
<PackageReference Include="YamlDotNet" Version="9.1.4"/>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="help.yaml"/>
</ItemGroup>
</Project>

View File

@ -1,10 +1,10 @@
author: ascclemens
name: HUD Manager
description: |
A plugin to manage your HUD.
description: |-
A plugin to manage your HUD.
- Save infinite HUD layouts
- Swap between HUD layouts based on conditions
- Edit HUD elements precisely without /hudlayout
- Export and import layouts
- Save infinite HUD layouts
- Swap between HUD layouts based on conditions
- Edit HUD elements precisely without /hudlayout
- Export and import layouts
repo_url: https://git.sr.ht/~jkcclemens/HUDManager

15
HUD Manager/Help.cs Executable file
View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
namespace HUD_Manager {
[Serializable]
public class HelpFile {
public List<HelpEntry> Help { get; set; } = new();
}
[Serializable]
public class HelpEntry {
public string Name { get; set; } = null!;
public string Description { get; set; } = null!;
}
}

View File

@ -1,7 +1,11 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using HUD_Manager.Configuration;
using HUD_Manager.Structs;
using HUD_Manager.Tree;
namespace HUD_Manager {
public class Hud {
@ -110,7 +114,7 @@ namespace HUD_Manager {
var slotLayout = this.ReadLayout(slot);
for (var i = 0; i < slotLayout.elements.Length; i++) {
if (dict.TryGetValue(slotLayout.elements[i].id, out var element)) {
slotLayout.elements[i] = element;
slotLayout.elements[i] = new RawElement(element);
}
}
@ -124,6 +128,41 @@ namespace HUD_Manager {
this.SelectSlot(currentSlot, true);
}
}
public void WriteEffectiveLayout(HudSlot slot, Guid id) {
// find the node for this id
var nodes = Node<SavedLayout>.BuildTree(this.Plugin.Config.Layouts);
var node = nodes.Find(id);
if (node == null) {
return;
}
var elements = new Dictionary<ElementKind, Element>();
// get the ancestors and their elements for this node
foreach (var ancestor in node.Ancestors().Reverse()) {
foreach (var element in ancestor.Value.Elements) {
elements[element.Key] = element.Value;
}
}
// apply this node's elements
foreach (var element in node.Value.Elements) {
elements[element.Key] = element.Value;
}
var elemList = elements.Values.ToList();
while (elemList.Count < 81) {
elemList.Add(new Element(new RawElement()));
}
var effective = new Layout {
elements = elemList.Select(elem => new RawElement(elem)).ToArray(),
};
this.WriteLayout(slot, effective);
}
}
public enum HudSlot {

22
HUD Manager/Lumina/HudSheet.cs Executable file
View File

@ -0,0 +1,22 @@
using Lumina.Data;
using Lumina.Excel;
using Lumina.Text;
namespace HUD_Manager.Lumina {
[Sheet("Hud")]
public class HudSheet : IExcelRow {
public uint RowId { get; set; }
public uint SubRowId { get; set; }
public string Name { get; set; } = null!;
public string ShortName { get; set; } = null!;
public string ShorterName { get; set; } = null!;
public void PopulateData(RowParser parser, global::Lumina.Lumina lumina, Language language) {
this.RowId = parser.Row;
this.SubRowId = parser.SubRow;
this.Name = parser.ReadColumn<SeString>(0);
this.ShortName = parser.ReadColumn<SeString>(1);
this.ShorterName = parser.ReadColumn<SeString>(2);
}
}
}

View File

@ -4,6 +4,9 @@ using System.Linq;
using Dalamud.Game.Command;
using Dalamud.Plugin;
using HUD_Manager.Configuration;
using Resourcer;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace HUD_Manager {
public class Plugin : IDalamudPlugin {
@ -17,6 +20,7 @@ namespace HUD_Manager {
public Statuses Statuses { get; private set; } = null!;
public GameFunctions GameFunctions { get; private set; } = null!;
public Config Config { get; private set; } = null!;
public HelpFile Help { get; private set; } = null!;
public void Initialize(DalamudPluginInterface pluginInterface) {
this.Interface = pluginInterface;
@ -25,6 +29,11 @@ namespace HUD_Manager {
this.Config.Initialize(this.Interface);
this.Config.Save();
var deserializer = new DeserializerBuilder()
.WithNamingConvention(UnderscoredNamingConvention.Instance)
.Build();
this.Help = deserializer.Deserialize<HelpFile>(Resource.AsString("help.yaml"));
this.Ui = new PluginUi(this);
this.Hud = new Hud(this);
this.Statuses = new Statuses(this);
@ -46,7 +55,7 @@ namespace HUD_Manager {
this.Interface.UiBuilder.OnOpenConfigUi += this.Ui.ConfigUi;
this.Interface.Framework.OnUpdateEvent += this.Swapper.OnFrameworkUpdate;
this.Interface.CommandManager.AddHandler("/hud", new CommandInfo(this.OnCommand) {
this.Interface.CommandManager.AddHandler("/hudman", new CommandInfo(this.OnCommand) {
HelpMessage = "Open the HUD Manager settings or swap to layout name",
});
}
@ -55,7 +64,7 @@ namespace HUD_Manager {
this.Interface.UiBuilder.OnBuildUi -= this.Ui.Draw;
this.Interface.UiBuilder.OnOpenConfigUi -= this.Ui.ConfigUi;
this.Interface.Framework.OnUpdateEvent -= this.Swapper.OnFrameworkUpdate;
this.Interface.CommandManager.RemoveHandler("/hud");
this.Interface.CommandManager.RemoveHandler("/hudman");
}
private void OnCommand(string command, string args) {

View File

@ -7,10 +7,9 @@ using Dalamud.Interface;
using Dalamud.Plugin;
using HUD_Manager.Configuration;
using HUD_Manager.Structs;
using HUD_Manager.Tree;
using ImGuiNET;
using Lumina.Excel.GeneratedSheets;
using Newtonsoft.Json;
using Action = System.Action;
// TODO: Zone swaps?
@ -25,6 +24,32 @@ namespace HUD_Manager {
"ChatLogPanel_3",
};
private static readonly float[] ScaleOptions = {
2.0f,
1.8f,
1.6f,
1.4f,
1.2f,
1.1f,
1.0f,
0.9f,
0.8f,
0.6f,
};
private static readonly string[] ScaleOptionsNames = {
"200%",
"180%",
"160%",
"140%",
"120%",
"110%",
"100%",
"90%",
"80%",
"60%",
};
private Plugin Plugin { get; }
private bool _settingsVisible;
@ -34,10 +59,16 @@ namespace HUD_Manager {
set => this._settingsVisible = value;
}
private string _importName = "";
private string _renameName = "";
private string? _editorSearch;
private float _dragSpeed = 1.0f;
private string? _importName;
private Guid _selectedLayoutId = Guid.Empty;
private string? _newLayoutName;
private string? _renameLayout;
private Guid _selectedEditLayout = Guid.Empty;
private SavedLayout? SelectedSavedLayout => this._selectedLayoutId == Guid.Empty ? null : this.Plugin.Config.Layouts[this._selectedLayoutId];
private int _editingConditionIndex = -1;
@ -53,27 +84,6 @@ namespace HUD_Manager {
}
private void DrawSettings() {
void DrawHudSlotButtons(string idSuffix, Action<HudSlot> hudSlotAction, Action clipboardAction) {
var slotButtonSize = new Vector2(40, 0);
foreach (HudSlot slot in Enum.GetValues(typeof(HudSlot))) {
// Surround the button with parentheses if this is the current slot
var slotText = slot == this.Plugin.Hud.GetActiveHudSlot() ? $"({(int) slot + 1})" : ((int) slot + 1).ToString();
var buttonName = $"{slotText}##${idSuffix}";
if (ImGui.Button(buttonName, slotButtonSize)) {
PluginLog.Log("Importing outer");
hudSlotAction(slot);
}
ImGui.SameLine();
}
ImGui.SameLine();
if (ImGui.Button($"Clipboard##{idSuffix}")) {
clipboardAction();
}
}
if (!this.SettingsVisible) {
return;
}
@ -90,17 +100,17 @@ namespace HUD_Manager {
ImGui.TextColored(new Vector4(1f, 0f, 0f, 1f), "Read this first");
ImGui.Separator();
ImGui.PushTextWrapPos(ImGui.GetFontSize() * 20f);
ImGui.Text("HUD Manager will use the configured staging slot as its own slot to make changes to. This means the staging slot will be overwritten whenever any swap happens.");
ImGui.TextUnformatted("HUD Manager will use the configured staging slot as its own slot to make changes to. This means the staging slot will be overwritten whenever any swap happens.");
ImGui.Spacing();
ImGui.Text("Any HUD layout changes you make while HUD Manager is enabled may potentially be lost, no matter what slot. If you want to make changes to your HUD layout, TURN OFF HUD Manager first.");
ImGui.TextUnformatted("Any HUD layout changes you make while HUD Manager is enabled may potentially be lost, no matter what slot. If you want to make changes to your HUD layout, TURN OFF HUD Manager first.");
ImGui.Spacing();
ImGui.Text("When editing or making a new layout, to be completely safe, turn off swaps, set up your layout, import the layout into HUD Manager, then turn on swaps.");
ImGui.TextUnformatted("When editing or making a new layout, to be completely safe, turn off swaps, set up your layout, import the layout into HUD Manager, then turn on swaps.");
ImGui.Spacing();
ImGui.Text("If you are a new user, HUD Manager auto-imported your existing layouts on startup.");
ImGui.TextUnformatted("If you are a new user, HUD Manager auto-imported your existing layouts on startup.");
ImGui.Spacing();
ImGui.Text("Finally, HUD Manager is beta software. Back up your character data before using this plugin. You may lose some to all of your HUD layouts while testing this plugin.");
ImGui.TextUnformatted("Finally, HUD Manager is beta software. Back up your character data before using this plugin. You may lose some to all of your HUD layouts while testing this plugin.");
ImGui.Separator();
ImGui.Text("If you have read all of the above and are okay with continuing, check the box below to enable HUD Manager. You only need to do this once.");
ImGui.TextUnformatted("If you have read all of the above and are okay with continuing, check the box below to enable HUD Manager. You only need to do this once.");
ImGui.PopTextWrapPos();
var understandsRisks = this.Plugin.Config.UnderstandsRisks;
if (ImGui.Checkbox("I understand", ref understandsRisks)) {
@ -116,266 +126,14 @@ namespace HUD_Manager {
return;
}
if (ImGui.BeginTabItem("Layouts")) {
ImGui.Text("Saved layouts");
if (this.Plugin.Config.Layouts.Count == 0) {
ImGui.Text("None saved!");
} else {
ImGui.PushItemWidth(-1);
if (ImGui.ListBoxHeader("##saved-layouts")) {
foreach (var entry in this.Plugin.Config.Layouts) {
if (!ImGui.Selectable($"{entry.Value.Name}##{entry.Key}", this._selectedLayoutId == entry.Key)) {
continue;
}
this.DrawLayoutEditor();
this._selectedLayoutId = entry.Key;
this._renameName = entry.Value.Name;
this._importName = this._renameName;
}
this.DrawSwaps();
ImGui.ListBoxFooter();
}
ImGui.PopItemWidth();
ImGui.PushItemWidth(200);
ImGui.InputText("##rename-input", ref this._renameName, 100);
ImGui.PopItemWidth();
ImGui.SameLine();
if (ImGui.Button("Rename") && this._renameName.Length != 0 && this.SelectedSavedLayout != null) {
var layout = this.Plugin.Config.Layouts[this._selectedLayoutId];
var newLayout = new SavedLayout(this._renameName, layout.ToLayout(), layout.Positions);
this.Plugin.Config.Layouts[this._selectedLayoutId] = newLayout;
this.Plugin.Config.Save();
}
const int layoutActionButtonWidth = 30;
// `layoutActionButtonWidth` must be multiplied by however many action buttons there are here
ImGui.SameLine(ImGui.GetWindowContentRegionWidth() - layoutActionButtonWidth * 1);
ImGui.PushFont(UiBuilder.IconFont);
if (ImGui.Button(FontAwesomeIcon.Trash.ToIconString(), new Vector2(layoutActionButtonWidth, 0)) &&
this.SelectedSavedLayout != null) {
this.Plugin.Config.Layouts.Remove(this._selectedLayoutId);
this.Plugin.Config.HudConditionMatches.RemoveAll(m => m.LayoutId == this._selectedLayoutId);
this._selectedLayoutId = Guid.Empty;
this._renameName = "";
this.Plugin.Config.Save();
}
ImGui.PopFont();
ImGui.Text("Copy to...");
DrawHudSlotButtons("copy", slot => {
if (this.SelectedSavedLayout == null) {
return;
}
this.Plugin.Hud.WriteLayout(slot, this.SelectedSavedLayout.ToLayout());
}, () => {
if (this.SelectedSavedLayout == null) {
return;
}
var json = JsonConvert.SerializeObject(this.SelectedSavedLayout);
ImGui.SetClipboardText(json);
});
}
ImGui.Separator();
ImGui.Text("Import");
ImGui.InputText("Imported layout name", ref this._importName, 100);
var importPositions = this.Plugin.Config.ImportPositions;
if (ImGui.Checkbox("Import window positions", ref importPositions)) {
this.Plugin.Config.ImportPositions = importPositions;
this.Plugin.Config.Save();
}
ImGui.SameLine();
HelpMarker("If this is checked, the position of the chat box and the map will be saved with the imported layout.");
var isOverwriting = this.Plugin.Config.Layouts.Values.Any(layout => layout.Name == this._importName);
ImGui.Text((isOverwriting ? "Overwrite" : "Import") + " from...");
DrawHudSlotButtons("import", slot => {
PluginLog.Log("Importing inner");
this.ImportSlot(this._importName, slot);
this._importName = "";
}, () => {
SavedLayout? shared = null;
try {
shared = JsonConvert.DeserializeObject<SavedLayout>(ImGui.GetClipboardText());
} catch (Exception) {
// ignored
}
if (shared == null) {
return;
}
this.Import(this._importName, shared.ToLayout(), shared.Positions);
this._importName = "";
});
ImGui.EndTabItem();
}
if (ImGui.BeginTabItem("Swaps")) {
var enabled = this.Plugin.Config.SwapsEnabled;
if (ImGui.Checkbox("Enable swaps", ref enabled)) {
this.Plugin.Config.SwapsEnabled = enabled;
this.Plugin.Config.Save();
}
ImGui.Text("Note: Disable swaps when editing your HUD.");
ImGui.Spacing();
var staging = ((int) this.Plugin.Config.StagingSlot + 1).ToString();
if (ImGui.BeginCombo("Staging slot", staging)) {
foreach (HudSlot slot in Enum.GetValues(typeof(HudSlot))) {
if (!ImGui.Selectable(((int) slot + 1).ToString())) {
continue;
}
this.Plugin.Config.StagingSlot = slot;
this.Plugin.Config.Save();
}
ImGui.EndCombo();
}
ImGui.SameLine();
HelpMarker("The staging slot is the HUD layout slot that will be used as your HUD layout. All changes will be written to this slot when swaps are enabled.");
ImGui.Separator();
if (this.Plugin.Config.Layouts.Count == 0) {
ImGui.Text("Create at least one layout to begin setting up swaps.");
} else {
ImGui.Text("Add swap conditions below.\nThe first condition that is satisfied will be the layout that is used.");
ImGui.Separator();
this.DrawConditionsTable();
}
ImGui.EndTabItem();
}
this.DrawHelp();
#if DEBUG
if (ImGui.BeginTabItem("Debug")) {
ImGui.TextUnformatted("Print layout pointer address");
if (ImGui.Button("1")) {
var ptr = this.Plugin.Hud.GetLayoutPointer(HudSlot.One);
this.Plugin.Interface.Framework.Gui.Chat.Print($"{ptr.ToInt64():x}");
}
ImGui.SameLine();
if (ImGui.Button("2")) {
var ptr = this.Plugin.Hud.GetLayoutPointer(HudSlot.Two);
this.Plugin.Interface.Framework.Gui.Chat.Print($"{ptr.ToInt64():x}");
}
ImGui.SameLine();
if (ImGui.Button("3")) {
var ptr = this.Plugin.Hud.GetLayoutPointer(HudSlot.Three);
this.Plugin.Interface.Framework.Gui.Chat.Print($"{ptr.ToInt64():x}");
}
ImGui.SameLine();
if (ImGui.Button("4")) {
var ptr = this.Plugin.Hud.GetLayoutPointer(HudSlot.Four);
this.Plugin.Interface.Framework.Gui.Chat.Print($"{ptr.ToInt64():x}");
}
if (ImGui.Button("Save layout")) {
var ptr = this.Plugin.Hud.GetLayoutPointer(HudSlot.One);
var layout = Marshal.PtrToStructure<Layout>(ptr);
this.PreviousLayout = layout;
}
ImGui.SameLine();
if (ImGui.Button("Find difference")) {
var ptr = this.Plugin.Hud.GetLayoutPointer(HudSlot.One);
var layout = Marshal.PtrToStructure<Layout>(ptr);
foreach (var prevElem in this.PreviousLayout.elements) {
var currElem = layout.elements.FirstOrDefault(el => el.id == prevElem.id);
if (currElem.visibility == prevElem.visibility && !(Math.Abs(currElem.x - prevElem.x) > .01)) {
continue;
}
PluginLog.Log(currElem.id.ToString());
this.Plugin.Interface.Framework.Gui.Chat.Print(currElem.id.ToString());
}
}
if (ImGui.BeginChild("ui-elements", new Vector2(0, 0))) {
var ptr = this.Plugin.Hud.GetLayoutPointer(this.Plugin.Hud.GetActiveHudSlot());
var layout = Marshal.PtrToStructure<Layout>(ptr);
var changed = false;
var elements = (ElementKind[]) Enum.GetValues(typeof(ElementKind));
foreach (var kind in elements.OrderBy(el => el.ToString())) {
for (var i = 0; i < layout.elements.Length; i++) {
if (layout.elements[i].id != kind) {
continue;
}
ImGui.TextUnformatted(kind.ToString());
var x = layout.elements[i].x;
if (ImGui.DragFloat($"X##{kind}", ref x)) {
layout.elements[i].x = x;
changed = true;
}
var y = layout.elements[i].y;
if (ImGui.DragFloat($"Y##{kind}", ref y)) {
layout.elements[i].y = y;
changed = true;
}
var visible = layout.elements[i].visibility == Visibility.Visible;
if (ImGui.Checkbox($"Visible##{kind}", ref visible)) {
layout.elements[i].visibility = visible ? Visibility.Visible : Visibility.Hidden;
changed = true;
}
var scale = layout.elements[i].scale;
if (ImGui.DragFloat($"Scale##{kind}", ref scale)) {
layout.elements[i].scale = scale;
changed = true;
}
var opacity = (int) layout.elements[i].opacity;
if (ImGui.DragInt($"Opacity##{kind}", ref opacity, 1, 1, 255)) {
layout.elements[i].opacity = (byte) opacity;
changed = true;
}
ImGui.Separator();
break;
}
}
if (changed) {
Marshal.StructureToPtr(layout, ptr, false);
this.Plugin.Hud.SelectSlot(this.Plugin.Hud.GetActiveHudSlot(), true);
}
ImGui.EndChild();
}
ImGui.EndTabItem();
}
this.DrawDebug();
#endif
ImGui.EndTabBar();
@ -384,8 +142,508 @@ namespace HUD_Manager {
ImGui.End();
}
private void DrawSwaps() {
if (!ImGui.BeginTabItem("Swaps")) {
return;
}
var enabled = this.Plugin.Config.SwapsEnabled;
if (ImGui.Checkbox("Enable swaps", ref enabled)) {
this.Plugin.Config.SwapsEnabled = enabled;
this.Plugin.Config.Save();
this.Plugin.Statuses.SetHudLayout(this.Plugin.Interface.ClientState.LocalPlayer, true);
}
ImGui.TextUnformatted("Note: Disable swaps when editing your HUD.");
ImGui.Spacing();
var staging = ((int) this.Plugin.Config.StagingSlot + 1).ToString();
if (ImGui.BeginCombo("Staging slot", staging)) {
foreach (HudSlot slot in Enum.GetValues(typeof(HudSlot))) {
if (!ImGui.Selectable(((int) slot + 1).ToString())) {
continue;
}
this.Plugin.Config.StagingSlot = slot;
this.Plugin.Config.Save();
}
ImGui.EndCombo();
}
ImGui.SameLine();
HelpMarker("The staging slot is the HUD layout slot that will be used as your HUD layout. All changes will be written to this slot when swaps are enabled.");
ImGui.Separator();
if (this.Plugin.Config.Layouts.Count == 0) {
ImGui.TextUnformatted("Create at least one layout to begin setting up swaps.");
} else {
ImGui.TextUnformatted("Add swap conditions below.\nThe first condition that is satisfied will be the layout that is used.");
ImGui.Separator();
this.DrawConditionsTable();
}
ImGui.EndTabItem();
}
private void DrawLayoutEditor() {
if (!ImGui.BeginTabItem("Layout editor")) {
return;
}
if (this.Plugin.Config.SwapsEnabled) {
ImGui.TextUnformatted("Cannot edit layouts while swaps are enabled.");
if (ImGui.Button("Disable swaps")) {
this.Plugin.Config.SwapsEnabled = false;
this.Plugin.Config.Save();
}
goto EndTabItem;
}
var update = false;
ImGui.TextUnformatted("Layout");
var nodes = Node<SavedLayout>.BuildTree(this.Plugin.Config.Layouts);
this.Plugin.Config.Layouts.TryGetValue(this._selectedEditLayout, out var selected);
var selectedName = selected?.Name ?? "<none>";
if (ImGui.BeginCombo("##edit-layout", selectedName)) {
if (ImGui.Selectable("<none>")) {
this._selectedEditLayout = Guid.Empty;
}
foreach (var node in nodes) {
foreach (var (child, depth) in node.TraverseWithDepth()) {
var indent = new string(' ', (int) depth * 4);
if (!ImGui.Selectable($"{indent}{child.Value.Name}##edit-{child.Id}")) {
continue;
}
this._selectedEditLayout = child.Id;
update = true;
}
}
ImGui.EndCombo();
}
ImGui.SameLine();
if (IconButton(FontAwesomeIcon.Plus, "uimanager-add-layout")) {
ImGui.OpenPopup(Popups.AddLayout);
}
HoverTooltip("Add a new layout");
this.SetUpAddLayoutPopup();
ImGui.SameLine();
if (IconButton(FontAwesomeIcon.TrashAlt, "uimanager-delete-layout") && this._selectedEditLayout != Guid.Empty) {
ImGui.OpenPopup(Popups.DeleteVerify);
}
this.SetUpDeleteVerifyPopup(nodes);
HoverTooltip("Delete the selected layout");
ImGui.SameLine();
if (IconButton(FontAwesomeIcon.PencilAlt, "uimanager-rename-layout") && this._selectedEditLayout != Guid.Empty) {
this._renameLayout = this.Plugin.Config.Layouts[this._selectedEditLayout].Name;
ImGui.OpenPopup(Popups.RenameLayout);
}
HoverTooltip("Rename the selected layout");
this.SetUpRenameLayoutPopup();
ImGui.SameLine();
if (IconButton(FontAwesomeIcon.FileImport, "uimanager-import-layout")) {
ImGui.OpenPopup(Popups.ImportLayout);
}
HoverTooltip("Import a layout from an in-game HUD slot");
this.SetUpImportLayoutPopup();
if (this._selectedEditLayout == Guid.Empty) {
goto EndTabItem;
}
var layout = this.Plugin.Config.Layouts[this._selectedEditLayout];
this.Plugin.Config.Layouts.TryGetValue(layout.Parent, out var parent);
var parentName = parent?.Name ?? "<none>";
if (ImGui.BeginCombo("Parent", parentName)) {
if (ImGui.Selectable("<none>")) {
layout.Parent = Guid.Empty;
this.Plugin.Config.Save();
}
foreach (var node in nodes) {
foreach (var (child, depth) in node.TraverseWithDepth()) {
var indent = new string(' ', (int) depth * 4);
if (!ImGui.Selectable($"{indent}{child.Value.Name}##parent-{child.Id}") || child.Id == this._selectedEditLayout) {
continue;
}
layout.Parent = child.Id;
this.Plugin.Config.Save();
}
}
ImGui.EndCombo();
}
ImGui.Separator();
ImGui.TextUnformatted("Search");
ImGui.PushItemWidth(-1);
var search = this._editorSearch ?? string.Empty;
if (ImGui.InputText("##ui-editor-search", ref search, 100)) {
this._editorSearch = string.IsNullOrWhiteSpace(search) ? null : search;
}
ImGui.PopItemWidth();
ImGui.DragFloat("Slider speed", ref this._dragSpeed, 0.01f, 0.01f, 10f);
ImGui.Separator();
ImGui.TextUnformatted("HUD Elements");
ImGui.SameLine();
if (IconButton(FontAwesomeIcon.Plus, "uimanager-add-hud-element")) {
ImGui.OpenPopup(Popups.AddElement);
}
HoverTooltip("Add a new HUD element to this layout");
if (ImGui.BeginPopup(Popups.AddElement)) {
var kinds = Enum.GetValues(typeof(ElementKind))
.Cast<ElementKind>()
.OrderBy(el => el.LocalisedName(this.Plugin.Interface.Data));
foreach (var kind in kinds) {
if (!ImGui.Selectable($"{kind.LocalisedName(this.Plugin.Interface.Data)}##{kind}")) {
continue;
}
var currentLayout = this.Plugin.Hud.ReadLayout(this.Plugin.Hud.GetActiveHudSlot());
var element = currentLayout.elements.FirstOrDefault(el => el.id == kind);
this.Plugin.Config.Layouts[this._selectedEditLayout].Elements[kind] = new Element(element);
ImGui.CloseCurrentPopup();
}
ImGui.EndPopup();
}
if (ImGui.BeginChild("uimanager-layout-editor-elements", new Vector2(0, 0))) {
var toRemove = new List<ElementKind>();
foreach (var entry in layout.Elements) {
var name = entry.Key.LocalisedName(this.Plugin.Interface.Data);
if (this._editorSearch != null && !name.ContainsIgnoreCase(this._editorSearch)) {
continue;
}
ImGui.TextUnformatted(name);
var element = entry.Value;
var visible = element.Visibility == Visibility.Visible;
if (ImGui.Checkbox($"Visible##{entry.Key}", ref visible)) {
element.Visibility = visible ? Visibility.Visible : Visibility.Hidden;
update = true;
}
ImGui.SameLine();
if (IconButton(FontAwesomeIcon.TrashAlt, $"uimanager-remove-element-{entry.Key}")) {
toRemove.Add(entry.Key);
}
var x = element.X;
if (ImGui.DragFloat($"X##{entry.Key}", ref x, this._dragSpeed)) {
element.X = x;
update = true;
}
var y = element.Y;
if (ImGui.DragFloat($"Y##{entry.Key}", ref y, this._dragSpeed)) {
element.Y = y;
update = true;
}
var scaleIdx = Array.IndexOf(ScaleOptions, element.Scale);
if (scaleIdx == -1) {
scaleIdx = 6;
}
if (ImGui.Combo($"Scale##{entry.Key}", ref scaleIdx, ScaleOptionsNames, ScaleOptionsNames.Length)) {
element.Scale = ScaleOptions[scaleIdx];
update = true;
}
var opacity = (int) element.Opacity;
if (ImGui.DragInt($"Opacity##{entry.Key}", ref opacity, 1, 1, 255)) {
element.Opacity = (byte) opacity;
update = true;
}
ImGui.Separator();
}
foreach (var remove in toRemove) {
layout.Elements.Remove(remove);
}
if (update) {
this.Plugin.Hud.WriteLayout(this.Plugin.Config.StagingSlot, layout.ToLayout());
this.Plugin.Hud.SelectSlot(this.Plugin.Config.StagingSlot, true);
}
ImGui.EndChild();
}
EndTabItem:
ImGui.EndTabItem();
}
private void SetUpImportLayoutPopup() {
if (!ImGui.BeginPopup(Popups.ImportLayout)) {
return;
}
var importName = this._importName ?? "";
if (ImGui.InputText("Imported layout name", ref importName, 100)) {
this._importName = string.IsNullOrWhiteSpace(importName) ? null : importName;
}
var exists = this.Plugin.Config.Layouts.Values.Any(layout => layout.Name == this._importName);
if (exists) {
ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(1f, .8f, .2f, 1f));
ImGui.TextUnformatted("This will overwrite an existing layout.");
ImGui.PopStyleColor();
}
var current = this.Plugin.Hud.GetActiveHudSlot();
foreach (var slot in (HudSlot[]) Enum.GetValues(typeof(HudSlot))) {
var name = current == slot ? $"({(int) slot + 1})" : $"{(int) slot + 1}";
if (ImGui.Button($"{name}##import-{slot}")) {
Guid id;
string newName;
Dictionary<string, Vector2<short>> positions;
if (exists) {
var overwriting = this.Plugin.Config.Layouts.First(entry => entry.Value.Name == this._importName);
id = overwriting.Key;
newName = overwriting.Value.Name;
positions = overwriting.Value.Positions;
} else {
id = Guid.NewGuid();
newName = this._importName!;
positions = new Dictionary<string, Vector2<short>>();
}
var currentLayout = this.Plugin.Hud.ReadLayout(slot);
var newLayout = new SavedLayout(newName, currentLayout, positions);
this.Plugin.Config.Layouts[id] = newLayout;
this.Plugin.Config.Save();
this._selectedEditLayout = id;
ImGui.CloseCurrentPopup();
}
ImGui.SameLine();
}
if (IconButton(FontAwesomeIcon.Clipboard, "import-clipboard")) {
}
ImGui.EndPopup();
}
private void SetUpRenameLayoutPopup() {
if (!ImGui.BeginPopup(Popups.RenameLayout)) {
return;
}
var name = this._renameLayout ?? "<none>";
if (ImGui.InputText("Name", ref name, 100)) {
this._renameLayout = string.IsNullOrWhiteSpace(name) ? null : name;
}
if (ImGui.Button("Rename") && this._renameLayout != null) {
this.Plugin.Config.Layouts[this._selectedEditLayout].Name = this._renameLayout;
this.Plugin.Config.Save();
ImGui.CloseCurrentPopup();
}
ImGui.EndPopup();
}
private void SetUpDeleteVerifyPopup(IEnumerable<Node<SavedLayout>> nodes) {
if (!ImGui.BeginPopupModal(Popups.DeleteVerify)) {
return;
}
if (this.Plugin.Config.Layouts.TryGetValue(this._selectedEditLayout, out var deleting)) {
ImGui.TextUnformatted($"Are you sure you want to delete the layout \"{deleting.Name}\"?");
if (ImGui.Button("Yes")) {
// unset the parent of any child layouts
var node = nodes.Find(this._selectedEditLayout);
if (node != null) {
foreach (var child in node.Children) {
child.Parent = null;
child.Value.Parent = Guid.Empty;
}
}
this.Plugin.Config.Layouts.Remove(this._selectedEditLayout);
this._selectedEditLayout = Guid.Empty;
this.Plugin.Config.Save();
ImGui.CloseCurrentPopup();
}
ImGui.SameLine();
if (ImGui.Button("No")) {
ImGui.CloseCurrentPopup();
}
}
ImGui.EndPopup();
}
private void SetUpAddLayoutPopup() {
if (!ImGui.BeginPopup(Popups.AddLayout)) {
return;
}
var name = this._newLayoutName ?? string.Empty;
if (ImGui.InputText("Name", ref name, 100)) {
this._newLayoutName = string.IsNullOrWhiteSpace(name) ? null : name;
}
var exists = this.Plugin.Config.Layouts.Values.Any(layout => layout.Name == this._newLayoutName);
if (exists) {
ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(1f, 0f, 0f, 1f));
ImGui.TextUnformatted("A layout with that name already exists.");
ImGui.PopStyleColor();
}
if (!exists && ImGui.Button("Add") && this._newLayoutName != null) {
// create the layout
var saved = new SavedLayout(this._newLayoutName, new Dictionary<ElementKind, Element>(), new Dictionary<string, Vector2<short>>(), Guid.Empty);
// reset the new layout name
this._newLayoutName = null;
// generate a new id
var id = Guid.NewGuid();
// add the layout and save the config
this.Plugin.Config.Layouts[id] = saved;
this.Plugin.Config.Save();
// switch the editor to the new layout
this._selectedEditLayout = id;
ImGui.CloseCurrentPopup();
}
ImGui.EndPopup();
}
private void DrawHelp() {
if (!ImGui.BeginTabItem("Help")) {
return;
}
ImGui.PushTextWrapPos();
foreach (var entry in this.Plugin.Help.Help) {
if (ImGui.CollapsingHeader(entry.Name)) {
ImGui.TextUnformatted(entry.Description.Replace("\n", "\n\n"));
}
}
ImGui.PopTextWrapPos();
ImGui.EndTabItem();
}
#if DEBUG
private Layout PreviousLayout { get; set; }
private void DrawDebug() {
if (!ImGui.BeginTabItem("Debug")) {
return;
}
ImGui.TextUnformatted("Print layout pointer address");
if (ImGui.Button("1")) {
var ptr = this.Plugin.Hud.GetLayoutPointer(HudSlot.One);
this.Plugin.Interface.Framework.Gui.Chat.Print($"{ptr.ToInt64():x}");
}
ImGui.SameLine();
if (ImGui.Button("2")) {
var ptr = this.Plugin.Hud.GetLayoutPointer(HudSlot.Two);
this.Plugin.Interface.Framework.Gui.Chat.Print($"{ptr.ToInt64():x}");
}
ImGui.SameLine();
if (ImGui.Button("3")) {
var ptr = this.Plugin.Hud.GetLayoutPointer(HudSlot.Three);
this.Plugin.Interface.Framework.Gui.Chat.Print($"{ptr.ToInt64():x}");
}
ImGui.SameLine();
if (ImGui.Button("4")) {
var ptr = this.Plugin.Hud.GetLayoutPointer(HudSlot.Four);
this.Plugin.Interface.Framework.Gui.Chat.Print($"{ptr.ToInt64():x}");
}
if (ImGui.Button("Save layout")) {
var ptr = this.Plugin.Hud.GetLayoutPointer(HudSlot.One);
var layout = Marshal.PtrToStructure<Layout>(ptr);
this.PreviousLayout = layout;
}
ImGui.SameLine();
if (ImGui.Button("Find difference")) {
var ptr = this.Plugin.Hud.GetLayoutPointer(HudSlot.One);
var layout = Marshal.PtrToStructure<Layout>(ptr);
foreach (var prevElem in this.PreviousLayout.elements) {
var currElem = layout.elements.FirstOrDefault(el => el.id == prevElem.id);
if (currElem.visibility == prevElem.visibility && !(Math.Abs(currElem.x - prevElem.x) > .01)) {
continue;
}
PluginLog.Log(currElem.id.ToString());
this.Plugin.Interface.Framework.Gui.Chat.Print(currElem.id.ToString());
}
}
ImGui.EndTabItem();
}
#endif
private void DrawConditionsTable() {
ImGui.PushFont(UiBuilder.IconFont);
var height = ImGui.GetContentRegionAvail().Y - ImGui.CalcTextSize(FontAwesomeIcon.Plus.ToIconString()).Y - ImGui.GetStyle().ItemSpacing.Y - ImGui.GetStyle().ItemInnerSpacing.Y * 2;
@ -401,16 +659,16 @@ namespace HUD_Manager {
conditions.Add(new HudConditionMatch());
}
ImGui.Text("Job");
ImGui.TextUnformatted("Job");
ImGui.NextColumn();
ImGui.Text("State");
ImGui.TextUnformatted("State");
ImGui.NextColumn();
ImGui.Text("Layout");
ImGui.TextUnformatted("Layout");
ImGui.NextColumn();
ImGui.Text("Options");
ImGui.TextUnformatted("Options");
ImGui.NextColumn();
ImGui.Separator();
@ -489,14 +747,14 @@ namespace HUD_Manager {
ImGui.SetScrollHereY();
}
} else {
ImGui.Text(item.cond.ClassJob ?? string.Empty);
ImGui.TextUnformatted(item.cond.ClassJob ?? string.Empty);
ImGui.NextColumn();
ImGui.Text(item.cond.Status?.Name() ?? string.Empty);
ImGui.TextUnformatted(item.cond.Status?.Name() ?? string.Empty);
ImGui.NextColumn();
this.Plugin.Config.Layouts.TryGetValue(item.cond.LayoutId, out var condLayout);
ImGui.Text(condLayout?.Name ?? string.Empty);
ImGui.TextUnformatted(condLayout?.Name ?? string.Empty);
ImGui.NextColumn();
if (IconButton(FontAwesomeIcon.PencilAlt, $"{item.i}")) {
@ -581,11 +839,29 @@ namespace HUD_Manager {
this.Plugin.Statuses.SetHudLayout(null);
}
private static bool IconButton(FontAwesomeIcon icon, string append = "") {
private static bool IconButton(FontAwesomeIcon icon, string? id = null) {
ImGui.PushFont(UiBuilder.IconFont);
var button = ImGui.Button($"{icon.ToIconString()}##{append}");
var text = icon.ToIconString();
if (id != null) {
text += $"##{id}";
}
var result = ImGui.Button(text);
ImGui.PopFont();
return button;
return result;
}
private static void HoverTooltip(string text) {
if (!ImGui.IsItemHovered()) {
return;
}
ImGui.BeginTooltip();
ImGui.TextUnformatted(text);
ImGui.EndTooltip();
}
private static void HelpMarker(string text) {
@ -636,4 +912,13 @@ namespace HUD_Manager {
}
}
}
public static class Popups {
public const string AddLayout = "uimanager-add-layout-popup";
public const string RenameLayout = "uimanager-rename-layout-popup";
public const string ImportLayout = "uimanager-import-layout-popup";
public const string AddElement = "uimanager-add-element-popup";
public const string DeleteVerify = "Delete layout?##uimanager-delete-layout-modal";
}
}

View File

@ -76,7 +76,7 @@ namespace HUD_Manager {
if (!this.Plugin.Config.Layouts.TryGetValue(layoutId, out var layout)) {
return; // FIXME: do something better
}
this.Plugin.Hud.WriteLayout(this.Plugin.Config.StagingSlot, layout.ToLayout());
this.Plugin.Hud.WriteEffectiveLayout(this.Plugin.Config.StagingSlot, layoutId);
this.Plugin.Hud.SelectSlot(this.Plugin.Config.StagingSlot, true);
foreach (var entry in layout.Positions) {

View File

@ -1,29 +0,0 @@
using System.Runtime.InteropServices;
namespace HUD_Manager.Structs {
[StructLayout(LayoutKind.Sequential)]
public struct Element {
// [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
// public byte[] unknown0;
public ElementKind id;
public float x;
public float y;
public float scale;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 9)]
public byte[] unknown4;
public Visibility visibility;
public byte unknown6;
public byte opacity;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
public byte[] unknown8;
}
}

View File

@ -1,4 +1,8 @@
namespace HUD_Manager.Structs {
using Dalamud.Data;
using HUD_Manager.Lumina;
using Lumina.Excel.GeneratedSheets;
namespace HUD_Manager.Structs {
public enum ElementKind : uint {
FocusTargetBar = 3264409695,
StatusInfoEnfeeblements = 511728259,
@ -76,4 +80,112 @@
TheFeastScore = 3622852831,
BattleHighGauge = 884971695,
}
public static class ElementKindExt {
public static string LocalisedName(this ElementKind kind, DataManager data) {
uint? id = kind switch {
ElementKind.Hotbar1 => 0,
ElementKind.Hotbar2 => 1,
ElementKind.Hotbar3 => 2,
ElementKind.Hotbar4 => 3,
ElementKind.Hotbar5 => 4,
ElementKind.Hotbar6 => 5,
ElementKind.Hotbar7 => 6,
ElementKind.Hotbar8 => 7,
ElementKind.Hotbar9 => 8,
ElementKind.Hotbar10 => 9,
ElementKind.PetHotbar => 10,
ElementKind.CrossHotbar => 11,
ElementKind.ProgressBar => 12,
ElementKind.TargetBar => 13,
ElementKind.FocusTargetBar => 14,
ElementKind.PartyList => 15,
ElementKind.EnemyList => 16,
ElementKind.ParameterBar => 17,
ElementKind.Notices => 18,
ElementKind.Minimap => 19,
ElementKind.MainMenu => 20,
ElementKind.ServerInfo => 21,
ElementKind.Gil => 22,
ElementKind.InventoryGrid => 23,
ElementKind.DutyList => 24,
ElementKind.ItemHelp => 25,
ElementKind.ActionHelp => 26,
ElementKind.LimitGauge => 27,
ElementKind.ExperienceBar => 28,
ElementKind.StatusEffects => 29,
ElementKind.AllianceList1 => 30,
ElementKind.AllianceList2 => 31,
// ElementKind.DutyList => 32, // listed twice?
// 33 is "Timers"
// 34-39 empty
ElementKind.OathGauge => 40,
// 41 is "LightningGauge" - guessing that's for GL
ElementKind.BeastGauge => 42,
ElementKind.DragonGauge => 43,
ElementKind.SongGauge => 44,
ElementKind.HealingGauge => 45,
ElementKind.ElementalGauge => 46,
ElementKind.AetherflowGaugeSch => 47, // order?
ElementKind.AetherflowGaugeSmn => 48, // order?
ElementKind.TranceGauge => 49,
ElementKind.FaerieGauge => 50,
ElementKind.NinkiGauge => 51,
ElementKind.HeatGauge => 52,
// 53 is empty
ElementKind.BloodGauge => 54,
ElementKind.ArcanaGauge => 55,
ElementKind.KenkiGauge => 56,
ElementKind.SenGauge => 57,
ElementKind.BalanceGauge => 58,
ElementKind.DutyGauge => 59,
ElementKind.DutyAction => 60,
ElementKind.ChakraGauge => 61,
ElementKind.HutonGauge => 62,
ElementKind.ScenarioGuide => 63,
ElementKind.RivalWingsGauges => 64,
ElementKind.RivalWingsAllianceList => 65,
ElementKind.RivalWingsTeamInfo => 66,
ElementKind.StatusInfoEnhancements => 67,
ElementKind.StatusInfoEnfeeblements => 68,
ElementKind.StatusInfoOther => 69,
ElementKind.TargetInfoStatus => 70,
ElementKind.TargetInfoProgressBar => 71,
ElementKind.TargetInfoHp => 72,
ElementKind.TheFeastScore => 73,
ElementKind.TheFeastAllyInfo => 74,
ElementKind.TheFeastEnemyInfo => 75,
ElementKind.RivalWingsStationInfo => 76,
ElementKind.RivalWingsMercenaryInfo => 77,
ElementKind.DarksideGauge => 78,
ElementKind.PowderGauge => 79,
ElementKind.StepGauge => 80,
ElementKind.FourfoldFeathers => 81,
ElementKind.BattleHighGauge => 82,
ElementKind.NewGamePlusGuide => 83,
ElementKind.CompressedAether => 84,
// 84 is "Ocean Fishing: Voyage Missions"
_ => null,
};
if (id == null) {
return kind.ToString();
}
var name = data.GetExcelSheet<HudSheet>().GetRow(id.Value).Name;
uint? jobId = kind switch {
ElementKind.AetherflowGaugeSmn => 27,
ElementKind.AetherflowGaugeSch => 28,
_ => null,
};
if (jobId != null) {
var abbr = data.GetExcelSheet<ClassJob>().GetRow(jobId.Value).Abbreviation;
name += $" ({abbr})";
}
return name;
}
}
}

View File

@ -5,7 +5,7 @@ namespace HUD_Manager.Structs {
[StructLayout(LayoutKind.Sequential)]
public struct Layout {
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 81)]
public Element[] elements;
public RawElement[] elements;
public Dictionary<ElementKind, Element> ToDictionary() {
// NOTE: not using ToDictionary here because duplicate keys are possible with old broken layouts
@ -15,7 +15,7 @@ namespace HUD_Manager.Structs {
continue;
}
dict[elem.id] = elem;
dict[elem.id] = new Element(elem);
}
return dict;

View File

@ -0,0 +1,70 @@
using System.Runtime.InteropServices;
namespace HUD_Manager.Structs {
[StructLayout(LayoutKind.Sequential)]
public struct RawElement {
public ElementKind id;
public float x;
public float y;
public float scale;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 9)]
public byte[] unknown4;
public Visibility visibility;
public byte unknown6;
public byte opacity;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
public byte[] unknown8;
public RawElement(Element element) {
this.id = element.Id;
this.x = element.X;
this.y = element.Y;
this.scale = element.Scale;
this.unknown4 = element.Unknown4;
this.visibility = element.Visibility;
this.unknown6 = element.Unknown6;
this.opacity = element.Opacity;
this.unknown8 = element.Unknown8;
}
}
public class Element {
public ElementKind Id { get; set; }
public float X { get; set; }
public float Y { get; set; }
public float Scale { get; set; }
public byte[] Unknown4 { get; set; }
public Visibility Visibility { get; set; }
public byte Unknown6 { get; set; }
public byte Opacity { get; set; }
public byte[] Unknown8 { get; set; }
public Element(RawElement raw) {
this.Id = raw.id;
this.X = raw.x;
this.Y = raw.y;
this.Scale = raw.scale;
this.Unknown4 = raw.unknown4;
this.Visibility = raw.visibility;
this.Unknown6 = raw.unknown6;
this.Opacity = raw.opacity;
this.Unknown8 = raw.unknown8;
}
}
}

View File

@ -1,5 +1,4 @@
using System;
using Dalamud.Game.Internal;
using Dalamud.Game.Internal;
namespace HUD_Manager {
public class Swapper {
@ -10,10 +9,6 @@ namespace HUD_Manager {
}
public void OnFrameworkUpdate(Framework framework) {
if (framework == null) {
throw new ArgumentNullException(nameof(framework), "Framework cannot be null");
}
if (!this.Plugin.Config.SwapsEnabled || !this.Plugin.Config.UnderstandsRisks) {
return;
}

115
HUD Manager/Tree/Node.cs Executable file
View File

@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.Linq;
using HUD_Manager.Configuration;
namespace HUD_Manager.Tree {
public class Node<T> {
public Guid Id { get; }
public Node<T>? Parent { get; set; }
public T Value { get; set; }
public List<Node<T>> Children { get; } = new();
public Node(Node<T>? parent, Guid id, T value) {
this.Id = id;
this.Parent = parent;
this.Value = value;
}
private Node(Guid id) {
this.Id = id;
this.Value = default!;
}
public Node<T>? Find(Guid id) {
if (this.Id == id) {
return this;
}
foreach (var child in this.Children) {
var result = child.Find(id);
if (result != null) {
return result;
}
}
return null;
}
public IEnumerable<Node<T>> Ancestors() {
var parent = this.Parent;
while (parent != null) {
yield return parent;
parent = parent.Parent;
}
}
public IEnumerable<Node<T>> Traverse() {
var stack = new Stack<Node<T>>();
stack.Push(this);
while (stack.Any()) {
var next = stack.Pop();
yield return next;
foreach (var child in next.Children) {
stack.Push(child);
}
}
}
public IEnumerable<Tuple<Node<T>, uint>> TraverseWithDepth() {
var stack = new Stack<Tuple<Node<T>, uint>>();
stack.Push(Tuple.Create(this, (uint) 0));
while (stack.Any()) {
var next = stack.Pop();
yield return next;
foreach (var child in next.Item1.Children) {
stack.Push(Tuple.Create(child, next.Item2 + 1));
}
}
}
public static List<Node<SavedLayout>> BuildTree(Dictionary<Guid, SavedLayout> layouts) {
var lookup = new Dictionary<Guid, Node<SavedLayout>>();
var rootNodes = new List<Node<SavedLayout>>();
foreach (var item in layouts) {
if (lookup.TryGetValue(item.Key, out var ourNode)) {
ourNode.Value = item.Value;
} else {
ourNode = new Node<SavedLayout>(null, item.Key, item.Value);
lookup[item.Key] = ourNode;
}
if (item.Value.Parent == Guid.Empty) {
rootNodes.Add(ourNode);
} else {
if (!lookup.TryGetValue(item.Value.Parent, out var parentNode)) {
// create preliminary parent
parentNode = new Node<SavedLayout>(item.Value.Parent);
lookup[item.Value.Parent] = parentNode;
}
parentNode.Children.Add(ourNode);
ourNode.Parent = parentNode;
}
}
return rootNodes;
}
}
public static class NodeExt {
public static Node<T>? Find<T>(this IEnumerable<Node<T>> nodes, Guid id) {
foreach (var node in nodes) {
var found = node.Find(id);
if (found != null) {
return found;
}
}
return null;
}
}
}

9
HUD Manager/Util.cs Executable file
View File

@ -0,0 +1,9 @@
using System.Globalization;
namespace HUD_Manager {
public static class Util {
public static bool ContainsIgnoreCase(this string haystack, string needle) {
return CultureInfo.InvariantCulture.CompareInfo.IndexOf(haystack, needle, CompareOptions.IgnoreCase) >= 0;
}
}
}

31
HUD Manager/help.yaml Executable file
View File

@ -0,0 +1,31 @@
help:
- name: Basic overview
description: >-
HUD Manager allows you to create infinite HUD layouts and
dynamically swap to them based on in-game conditions.
- name: What is the staging slot?
description: >-
The staging slot is the in-game HUD slot that the plugin will
update. For example, if it is set to 4, your in-game HUD slot
will always be 4 while the plugin is active. The plugin will
update that slot with whichever layout is appropriate when
necessary.
- name: What is a layout parent?/How does inheritance work?
description: >-
Every layout created by HUD Manager may have a parent. Layouts
with parents are called child layouts. Child layouts inherit all
HUD elements from all of their parents but can override
them. This system allows for you to create one base layout and
have several variations. When you update the base layout, all
the variations will reflect that update automatically without
any extra work on your part.
For example, imagine a layout called "Default" and a layout
called "Battle". The Battle layout is a child of the Default
layout and is set to turn on in combat. The Battle layout has
one HUD element added: gil, and gil is hidden. The Default
layout has gil visible. When not in combat, gil will be visible,
but when entering combat, gil will be hidden. This is because
the Battle layout inherits all the HUD elements from the Default
layout, but it overrides the gil HUD element to hide it during
combat.