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 79de60b449
commit 3fde80aed1
3 changed files with 227 additions and 77 deletions

View File

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

View File

@ -4,6 +4,8 @@ using System.Runtime.InteropServices;
namespace HudSwap {
public class HUD {
private const int LAYOUT_SIZE = 0xb40;
private delegate IntPtr GetFilePointerDelegate(byte index);
private delegate uint SetHudLayoutDelegate(IntPtr filePtr, uint hudLayout, byte unk0, byte unk1);
@ -28,9 +30,51 @@ namespace HudSwap {
return this._getFilePointer.Invoke(index);
}
public uint SetHudLayout(uint hudLayout) {
public uint SelectSlot(HudSlot slot, bool force = false) {
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 ImGuiNET;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace HudSwap {
@ -24,6 +25,9 @@ namespace HudSwap {
this.SettingsVisible = true;
}
private string importName = "";
private Guid selectedLayout = Guid.Empty;
public void DrawSettings() {
if (!this.SettingsVisible) {
return;
@ -32,72 +36,122 @@ namespace HudSwap {
PlayerCharacter player = this.pi.ClientState.LocalPlayer;
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.");
int defaultLayout = (int)this.plugin.config.DefaultLayout + 1;
if (ImGui.InputInt("##default-layout", ref defaultLayout)) {
defaultLayout = Math.Max(1, Math.Min(4, defaultLayout));
this.plugin.config.DefaultLayout = (uint)defaultLayout - 1;
this.plugin.config.Save();
this.statuses.SetHudLayout(player, true);
}
if (ImGui.BeginTabBar("##hudswap-tabs")) {
if (ImGui.BeginTabItem("Layouts")) {
ImGui.Text("Saved layouts");
if (this.plugin.config.Layouts.Keys.Count == 0) {
ImGui.Text("None saved!");
} else {
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("These settings are ordered from highest priority to lowest priority.\nHigher priorities overwrite lower priorities when enabled.");
ImGui.Text("Copy onto slot...");
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.Checkbox("Change HUD when combat begins", ref onCombat)) {
this.plugin.config.ChangeOnCombat = onCombat;
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);
}
if (ImGui.Button("Delete") && this.selectedLayout != null) {
this.plugin.config.Layouts.Remove(this.selectedLayout);
this.selectedLayout = Guid.Empty;
this.plugin.config.Save();
}
}
ImGui.Spacing();
ImGui.Separator();
bool onWeaponDrawn = this.plugin.config.ChangeOnWeaponDrawn;
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.Text("Import");
ImGui.Spacing();
ImGui.InputText("Imported layout name", ref this.importName, 100);
bool inInstance = this.plugin.config.ChangeOnInstance;
if (ImGui.Checkbox("Change HUD when in instance", ref inInstance)) {
this.plugin.config.ChangeOnInstance = inInstance;
this.plugin.config.Save();
this.statuses.SetHudLayout(player, true);
}
int instanceLayout = (int)this.plugin.config.InstanceLayout + 1;
if (ImGui.InputInt("##instance-layout", ref instanceLayout)) {
instanceLayout = Math.Max(1, Math.Min(4, instanceLayout));
this.plugin.config.InstanceLayout = (uint)instanceLayout - 1;
this.plugin.config.Save();
this.statuses.SetHudLayout(player, true);
foreach (HudSlot slot in Enum.GetValues(typeof(HudSlot))) {
string buttonName = $"{(int)slot + 1}##import";
if (ImGui.Button(buttonName) && this.importName != "") {
this.plugin.config.Layouts[Guid.NewGuid()] = new Tuple<string, byte[]>(this.importName, this.plugin.hud.ReadLayout(slot));
this.importName = "";
this.plugin.config.Save();
}
if (slot != HudSlot.Four) {
ImGui.SameLine();
}
}
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();
}
}
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() {
this.DrawSettings();
if (!this.plugin.config.SwapsEnabled) {
return;
}
PlayerCharacter player = this.pi.ClientState.LocalPlayer;
if (player == null) {
return;
@ -107,6 +161,28 @@ namespace HudSwap {
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 {
@ -116,6 +192,8 @@ namespace HudSwap {
public bool InCombat { get; private set; } = false;
public bool WeaponDrawn { 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) {
this.plugin = plugin;
@ -140,6 +218,18 @@ namespace HudSwap {
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) {
if (player == null) {
return false;
@ -147,34 +237,40 @@ namespace HudSwap {
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.SetInCombat(this.pi.ClientState.Condition[ConditionFlag.InCombat]) && this.plugin.config.ChangeOnCombat;
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.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;
}
public uint CalculateCurrentHud() {
public Guid CalculateCurrentHud() {
PlayerCharacter player = this.pi.ClientState.LocalPlayer;
if (player == null) {
return 0;
return Guid.Empty;
}
this.Update(player);
uint layout = this.plugin.config.DefaultLayout;
Guid layout = this.plugin.config.defaultLayout;
if (this.InInstance && this.plugin.config.ChangeOnInstance) {
layout = this.plugin.config.InstanceLayout;
if (this.Gathering && this.plugin.config.gatheringLayout != Guid.Empty) {
layout = this.plugin.config.gatheringLayout;
}
if (this.WeaponDrawn && this.plugin.config.ChangeOnWeaponDrawn) {
layout = this.plugin.config.WeaponDrawnLayout;
if (this.Crafting && this.plugin.config.craftingLayout != Guid.Empty) {
layout = this.plugin.config.craftingLayout;
}
if (this.InCombat && this.plugin.config.ChangeOnCombat) {
layout = this.plugin.config.CombatLayout;
if (this.InInstance && this.plugin.config.instanceLayout != Guid.Empty) {
layout = this.plugin.config.instanceLayout;
}
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;
@ -185,8 +281,16 @@ namespace HudSwap {
this.Update(player);
}
uint layout = this.CalculateCurrentHud();
this.plugin.hud.SetHudLayout(layout);
Guid layout = this.CalculateCurrentHud();
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) {