feat: infinite layout slots

Add a system to save layout slots and restore them, effectively
increasing the number of slots to an infinite amount.

Also add crafting and gathering layout swaps and make the UI more
reasonable.

Refactor out the combo boxes.
This commit is contained in:
Anna 2020-07-29 21:01:22 -04:00
parent 820aa94a65
commit 11c9d92f4e
Signed by: anna
GPG Key ID: 0B391D8F06FCD9E0
3 changed files with 227 additions and 77 deletions

View File

@ -1,6 +1,7 @@
using Dalamud.Configuration; using Dalamud.Configuration;
using Dalamud.Plugin; using Dalamud.Plugin;
using System; using System;
using System.Collections.Generic;
namespace HudSwap { namespace HudSwap {
[Serializable] [Serializable]
@ -10,16 +11,17 @@ namespace HudSwap {
[NonSerialized] [NonSerialized]
private DalamudPluginInterface pi; private DalamudPluginInterface pi;
public uint DefaultLayout { get; set; } = 0; public bool SwapsEnabled { get; set; } = false;
public bool ChangeOnCombat { get; set; } = false; public Guid defaultLayout = Guid.Empty;
public uint CombatLayout { get; set; } = 0;
public bool ChangeOnWeaponDrawn { get; set; } = false; public Guid combatLayout = Guid.Empty;
public uint WeaponDrawnLayout { get; set; } = 0; public Guid weaponDrawnLayout = Guid.Empty;
public Guid instanceLayout = Guid.Empty;
public Guid craftingLayout = Guid.Empty;
public Guid gatheringLayout = Guid.Empty;
public bool ChangeOnInstance { get; set; } = false; public Dictionary<Guid, Tuple<string, byte[]>> Layouts { get; } = new Dictionary<Guid, Tuple<string, byte[]>>();
public uint InstanceLayout { get; set; } = 0;
public void Initialize(DalamudPluginInterface pluginInterface) { public void Initialize(DalamudPluginInterface pluginInterface) {
this.pi = pluginInterface; this.pi = pluginInterface;

View File

@ -4,6 +4,8 @@ using System.Runtime.InteropServices;
namespace HudSwap { namespace HudSwap {
public class HUD { public class HUD {
private const int LAYOUT_SIZE = 0xb40;
private delegate IntPtr GetFilePointerDelegate(byte index); private delegate IntPtr GetFilePointerDelegate(byte index);
private delegate uint SetHudLayoutDelegate(IntPtr filePtr, uint hudLayout, byte unk0, byte unk1); private delegate uint SetHudLayoutDelegate(IntPtr filePtr, uint hudLayout, byte unk0, byte unk1);
@ -28,9 +30,51 @@ namespace HudSwap {
return this._getFilePointer.Invoke(index); return this._getFilePointer.Invoke(index);
} }
public uint SetHudLayout(uint hudLayout) { public uint SelectSlot(HudSlot slot, bool force = false) {
IntPtr file = this.GetFilePointer(0); IntPtr file = this.GetFilePointer(0);
return this._setHudLayout.Invoke(file, hudLayout, 0, 1); if (force) {
IntPtr currentSlotPtr = this.GetDataPointer() + 0x5958;
uint currentSlot = (uint)Marshal.ReadInt32(currentSlotPtr);
if (currentSlot < 3) {
currentSlot += 1;
} else {
currentSlot = 0;
}
Marshal.WriteInt32(currentSlotPtr, (int)currentSlot);
}
return this._setHudLayout.Invoke(file, (uint)slot, 0, 1);
}
private IntPtr GetDataPointer() {
IntPtr dataPtr = this.GetFilePointer(0) + 0x50;
return Marshal.ReadIntPtr(dataPtr);
}
private IntPtr GetLayoutPointer(HudSlot slot) {
int slotNum = (int)slot;
return this.GetDataPointer() + 0x2c58 + (slotNum * LAYOUT_SIZE);
}
public byte[] ReadLayout(HudSlot slot) {
IntPtr slotPtr = this.GetLayoutPointer(slot);
byte[] bytes = new byte[LAYOUT_SIZE];
Marshal.Copy(slotPtr, bytes, 0, LAYOUT_SIZE);
return bytes;
}
public void WriteLayout(HudSlot slot, byte[] layout) {
if (layout.Length != LAYOUT_SIZE) {
throw new ArgumentException($"layout must be {LAYOUT_SIZE} bytes", nameof(layout));
}
IntPtr slotPtr = this.GetLayoutPointer(slot);
Marshal.Copy(layout, 0, slotPtr, LAYOUT_SIZE);
} }
} }
public enum HudSlot {
One = 0,
Two = 1,
Three = 2,
Four = 3,
}
} }

View File

@ -3,6 +3,7 @@ using Dalamud.Game.ClientState.Actors.Types;
using Dalamud.Plugin; using Dalamud.Plugin;
using ImGuiNET; using ImGuiNET;
using System; using System;
using System.Collections.Generic;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace HudSwap { namespace HudSwap {
@ -24,6 +25,9 @@ namespace HudSwap {
this.SettingsVisible = true; this.SettingsVisible = true;
} }
private string importName = "";
private Guid selectedLayout = Guid.Empty;
public void DrawSettings() { public void DrawSettings() {
if (!this.SettingsVisible) { if (!this.SettingsVisible) {
return; return;
@ -32,72 +36,122 @@ namespace HudSwap {
PlayerCharacter player = this.pi.ClientState.LocalPlayer; PlayerCharacter player = this.pi.ClientState.LocalPlayer;
if (ImGui.Begin("HudSwap", ref this._settingsVisible, ImGuiWindowFlags.AlwaysAutoResize)) { if (ImGui.Begin("HudSwap", ref this._settingsVisible, ImGuiWindowFlags.AlwaysAutoResize)) {
ImGui.Text("This is the default layout. If none of the below conditions are\nsatisfied, this layout will be enabled."); if (ImGui.BeginTabBar("##hudswap-tabs")) {
int defaultLayout = (int)this.plugin.config.DefaultLayout + 1; if (ImGui.BeginTabItem("Layouts")) {
if (ImGui.InputInt("##default-layout", ref defaultLayout)) { ImGui.Text("Saved layouts");
defaultLayout = Math.Max(1, Math.Min(4, defaultLayout)); if (this.plugin.config.Layouts.Keys.Count == 0) {
this.plugin.config.DefaultLayout = (uint)defaultLayout - 1; ImGui.Text("None saved!");
this.plugin.config.Save(); } else {
this.statuses.SetHudLayout(player, true); if (ImGui.ListBoxHeader("##saved-layouts")) {
} foreach (KeyValuePair<Guid, Tuple<string, byte[]>> entry in this.plugin.config.Layouts) {
if (ImGui.Selectable(entry.Value.Item1, this.selectedLayout == entry.Key)) {
this.selectedLayout = entry.Key;
}
}
ImGui.ListBoxFooter();
}
ImGui.Spacing(); ImGui.Text("Copy onto slot...");
ImGui.Text("These settings are ordered from highest priority to lowest priority.\nHigher priorities overwrite lower priorities when enabled."); foreach (HudSlot slot in Enum.GetValues(typeof(HudSlot))) {
string buttonName = $"{(int)slot + 1}##copy";
if (ImGui.Button(buttonName) && this.selectedLayout != null) {
byte[] layout = this.plugin.config.Layouts[this.selectedLayout].Item2;
this.plugin.hud.WriteLayout(slot, layout);
}
ImGui.SameLine();
}
bool onCombat = this.plugin.config.ChangeOnCombat; if (ImGui.Button("Delete") && this.selectedLayout != null) {
if (ImGui.Checkbox("Change HUD when combat begins", ref onCombat)) { this.plugin.config.Layouts.Remove(this.selectedLayout);
this.plugin.config.ChangeOnCombat = onCombat; this.selectedLayout = Guid.Empty;
this.plugin.config.Save(); this.plugin.config.Save();
this.statuses.SetHudLayout(player, true); }
this.statuses.SetHudLayout(player, true); }
}
int combatLayout = (int)this.plugin.config.CombatLayout + 1;
if (ImGui.InputInt("##combat-layout", ref combatLayout)) {
combatLayout = Math.Max(1, Math.Min(4, combatLayout));
this.plugin.config.CombatLayout = (uint)combatLayout - 1;
this.plugin.config.Save();
this.statuses.SetHudLayout(player, true);
}
ImGui.Spacing(); ImGui.Separator();
bool onWeaponDrawn = this.plugin.config.ChangeOnWeaponDrawn; ImGui.Text("Import");
if (ImGui.Checkbox("Change HUD when weapon is drawn", ref onWeaponDrawn)) {
this.plugin.config.ChangeOnWeaponDrawn = onWeaponDrawn;
this.plugin.config.Save();
this.statuses.SetHudLayout(player, true);
}
int weaponLayout = (int)this.plugin.config.WeaponDrawnLayout + 1;
if (ImGui.InputInt("##weapon-layout", ref weaponLayout)) {
weaponLayout = Math.Max(1, Math.Min(4, weaponLayout));
this.plugin.config.WeaponDrawnLayout = (uint)weaponLayout - 1;
this.plugin.config.Save();
this.statuses.SetHudLayout(player, true);
}
ImGui.Spacing(); ImGui.InputText("Imported layout name", ref this.importName, 100);
bool inInstance = this.plugin.config.ChangeOnInstance; foreach (HudSlot slot in Enum.GetValues(typeof(HudSlot))) {
if (ImGui.Checkbox("Change HUD when in instance", ref inInstance)) { string buttonName = $"{(int)slot + 1}##import";
this.plugin.config.ChangeOnInstance = inInstance; if (ImGui.Button(buttonName) && this.importName != "") {
this.plugin.config.Save(); this.plugin.config.Layouts[Guid.NewGuid()] = new Tuple<string, byte[]>(this.importName, this.plugin.hud.ReadLayout(slot));
this.statuses.SetHudLayout(player, true); this.importName = "";
} this.plugin.config.Save();
int instanceLayout = (int)this.plugin.config.InstanceLayout + 1; }
if (ImGui.InputInt("##instance-layout", ref instanceLayout)) { if (slot != HudSlot.Four) {
instanceLayout = Math.Max(1, Math.Min(4, instanceLayout)); ImGui.SameLine();
this.plugin.config.InstanceLayout = (uint)instanceLayout - 1; }
this.plugin.config.Save(); }
this.statuses.SetHudLayout(player, true);
ImGui.EndTabItem();
}
if (ImGui.BeginTabItem("Swaps")) {
ImGui.Text("Disable swaps when editing your HUD.");
bool enabled = this.plugin.config.SwapsEnabled;
if (ImGui.Checkbox("Enabled", ref enabled)) {
this.plugin.config.SwapsEnabled = enabled;
this.plugin.config.Save();
}
ImGui.Separator();
ImGui.Text("This is the default layout. If none of the below conditions are\nsatisfied, this layout will be enabled.");
if (ImGui.BeginCombo("##default-layout", this.LayoutNameOrDefault(this.plugin.config.defaultLayout))) {
foreach (KeyValuePair<Guid, Tuple<string, byte[]>> entry in this.plugin.config.Layouts) {
if (ImGui.Selectable(entry.Value.Item1)) {
this.plugin.config.defaultLayout = entry.Key;
this.plugin.config.Save();
}
}
ImGui.EndCombo();
}
ImGui.Spacing();
ImGui.Text("These settings are ordered from highest priority to lowest priority.\nHigher priorities overwrite lower priorities when enabled.");
ImGui.Spacing();
ImGui.Columns(2);
this.LayoutBox("In combat", ref this.plugin.config.combatLayout, player);
this.LayoutBox("Weapon drawn", ref this.plugin.config.weaponDrawnLayout, player);
this.LayoutBox("In instance", ref this.plugin.config.instanceLayout, player);
this.LayoutBox("Crafting", ref this.plugin.config.craftingLayout, player);
this.LayoutBox("Gathering", ref this.plugin.config.gatheringLayout, player);
ImGui.Columns(1);
ImGui.EndTabItem();
}
ImGui.EndTabBar();
} }
ImGui.End(); ImGui.End();
} }
} }
private string LayoutNameOrDefault(Guid key) {
Tuple<string, byte[]> tuple;
if (this.plugin.config.Layouts.TryGetValue(key, out tuple)) {
return tuple.Item1;
} else {
return "";
}
}
public void Draw() { public void Draw() {
this.DrawSettings(); this.DrawSettings();
if (!this.plugin.config.SwapsEnabled) {
return;
}
PlayerCharacter player = this.pi.ClientState.LocalPlayer; PlayerCharacter player = this.pi.ClientState.LocalPlayer;
if (player == null) { if (player == null) {
return; return;
@ -107,6 +161,28 @@ namespace HudSwap {
this.statuses.SetHudLayout(null); this.statuses.SetHudLayout(null);
} }
} }
private void LayoutBox(string name, ref Guid layout, PlayerCharacter player) {
ImGui.Text(name);
ImGui.NextColumn();
if (ImGui.BeginCombo($"##{name}-layout", this.LayoutNameOrDefault(layout))) {
if (ImGui.Selectable("Not set")) {
layout = Guid.Empty;
this.plugin.config.Save();
this.statuses.SetHudLayout(player, true);
}
ImGui.Separator();
foreach (KeyValuePair<Guid, Tuple<string, byte[]>> entry in this.plugin.config.Layouts) {
if (ImGui.Selectable(entry.Value.Item1)) {
layout = entry.Key;
this.plugin.config.Save();
this.statuses.SetHudLayout(player, true);
}
}
ImGui.EndCombo();
}
ImGui.NextColumn();
}
} }
public class Statuses { public class Statuses {
@ -116,6 +192,8 @@ namespace HudSwap {
public bool InCombat { get; private set; } = false; public bool InCombat { get; private set; } = false;
public bool WeaponDrawn { get; private set; } = false; public bool WeaponDrawn { get; private set; } = false;
public bool InInstance { get; private set; } = false; public bool InInstance { get; private set; } = false;
public bool Crafting { get; private set; } = false;
public bool Gathering { get; private set; } = false;
public Statuses(HudSwapPlugin plugin, DalamudPluginInterface pi) { public Statuses(HudSwapPlugin plugin, DalamudPluginInterface pi) {
this.plugin = plugin; this.plugin = plugin;
@ -140,6 +218,18 @@ namespace HudSwap {
return old != this.InInstance; return old != this.InInstance;
} }
public bool SetCrafting(bool crafting) {
bool old = this.Crafting;
this.Crafting = crafting;
return old != this.Crafting;
}
public bool SetGathering(bool gathering) {
bool old = this.Gathering;
this.Gathering = gathering;
return old != this.Gathering;
}
public bool Update(PlayerCharacter player) { public bool Update(PlayerCharacter player) {
if (player == null) { if (player == null) {
return false; return false;
@ -147,34 +237,40 @@ namespace HudSwap {
bool anyChanged = false; bool anyChanged = false;
anyChanged |= this.SetInInstance(this.pi.ClientState.Condition[ConditionFlag.BoundByDuty]) && this.plugin.config.ChangeOnInstance; Condition condition = this.pi.ClientState.Condition;
anyChanged |= this.SetWeaponDrawn(this.IsWeaponDrawn(player)) && this.plugin.config.ChangeOnWeaponDrawn; anyChanged |= this.SetGathering(condition[ConditionFlag.Gathering]) && this.plugin.config.gatheringLayout != Guid.Empty;
anyChanged |= this.SetCrafting(condition[ConditionFlag.Crafting]) && this.plugin.config.craftingLayout != Guid.Empty;
anyChanged |= this.SetInCombat(this.pi.ClientState.Condition[ConditionFlag.InCombat]) && this.plugin.config.ChangeOnCombat; anyChanged |= this.SetInInstance(condition[ConditionFlag.BoundByDuty]) && this.plugin.config.instanceLayout != Guid.Empty;
anyChanged |= this.SetWeaponDrawn(this.IsWeaponDrawn(player)) && this.plugin.config.weaponDrawnLayout != Guid.Empty;
anyChanged |= this.SetInCombat(condition[ConditionFlag.InCombat]) && this.plugin.config.combatLayout != Guid.Empty;
return anyChanged; return anyChanged;
} }
public uint CalculateCurrentHud() { public Guid CalculateCurrentHud() {
PlayerCharacter player = this.pi.ClientState.LocalPlayer; PlayerCharacter player = this.pi.ClientState.LocalPlayer;
if (player == null) { if (player == null) {
return 0; return Guid.Empty;
} }
this.Update(player); this.Update(player);
uint layout = this.plugin.config.DefaultLayout; Guid layout = this.plugin.config.defaultLayout;
if (this.InInstance && this.plugin.config.ChangeOnInstance) { if (this.Gathering && this.plugin.config.gatheringLayout != Guid.Empty) {
layout = this.plugin.config.InstanceLayout; layout = this.plugin.config.gatheringLayout;
} }
if (this.Crafting && this.plugin.config.craftingLayout != Guid.Empty) {
if (this.WeaponDrawn && this.plugin.config.ChangeOnWeaponDrawn) { layout = this.plugin.config.craftingLayout;
layout = this.plugin.config.WeaponDrawnLayout;
} }
if (this.InInstance && this.plugin.config.instanceLayout != Guid.Empty) {
if (this.InCombat && this.plugin.config.ChangeOnCombat) { layout = this.plugin.config.instanceLayout;
layout = this.plugin.config.CombatLayout; }
if (this.WeaponDrawn && this.plugin.config.weaponDrawnLayout != Guid.Empty) {
layout = this.plugin.config.weaponDrawnLayout;
}
if (this.InCombat && this.plugin.config.combatLayout != Guid.Empty) {
layout = this.plugin.config.combatLayout;
} }
return layout; return layout;
@ -185,8 +281,16 @@ namespace HudSwap {
this.Update(player); this.Update(player);
} }
uint layout = this.CalculateCurrentHud(); Guid layout = this.CalculateCurrentHud();
this.plugin.hud.SetHudLayout(layout); if (layout == Guid.Empty) {
return; // FIXME: do something better
}
byte[] layoutBytes = this.plugin.config.Layouts[layout]?.Item2;
if (layoutBytes == null) {
return; // FIXME: do something better
}
this.plugin.hud.WriteLayout(HudSlot.Four, layoutBytes);
this.plugin.hud.SelectSlot(HudSlot.Four, true);
} }
private byte GetStatus(Actor actor) { private byte GetStatus(Actor actor) {