diff --git a/HudSwap/Configuration.cs b/HudSwap/Configuration.cs index de4824d..a8453ee 100644 --- a/HudSwap/Configuration.cs +++ b/HudSwap/Configuration.cs @@ -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> Layouts { get; } = new Dictionary>(); public void Initialize(DalamudPluginInterface pluginInterface) { this.pi = pluginInterface; diff --git a/HudSwap/HUD.cs b/HudSwap/HUD.cs index fa3b545..7c08d39 100644 --- a/HudSwap/HUD.cs +++ b/HudSwap/HUD.cs @@ -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, + } } diff --git a/HudSwap/PluginUI.cs b/HudSwap/PluginUI.cs index 7ab87c8..2f1b42c 100644 --- a/HudSwap/PluginUI.cs +++ b/HudSwap/PluginUI.cs @@ -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> 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(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> 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 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> 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) {