HUDManager/HudSwap/PluginUI.cs

356 lines
14 KiB
C#

using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Actors.Types;
using Dalamud.Plugin;
using ImGuiNET;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
// TODO: Job swaps?
// TODO: Zone swaps?
namespace HudSwap {
public class PluginUI {
private readonly HudSwapPlugin plugin;
private readonly DalamudPluginInterface pi;
private readonly Statuses statuses;
private bool _settingsVisible = false;
public bool SettingsVisible { get => this._settingsVisible; set => this._settingsVisible = value; }
public PluginUI(HudSwapPlugin plugin, DalamudPluginInterface pi) {
this.plugin = plugin;
this.pi = pi;
this.statuses = new Statuses(this.plugin, this.pi);
}
public void ConfigUI(object sender, EventArgs args) {
this.SettingsVisible = true;
}
private string importName = "";
private Guid selectedLayout = Guid.Empty;
private static bool configErrorOpen = true;
public static void ConfigError() {
if (ImGui.Begin("HudSwap error", ref configErrorOpen)) {
ImGui.Text("Could not load HudSwap configuration.");
ImGui.Spacing();
ImGui.Text("If you are updating from a previous version, please\ndelete your configuration file and restart the game.");
ImGui.End();
}
}
public void DrawSettings() {
if (!this.SettingsVisible) {
return;
}
PlayerCharacter player = this.pi.ClientState.LocalPlayer;
if (ImGui.Begin("HudSwap", ref this._settingsVisible, ImGuiWindowFlags.AlwaysAutoResize)) {
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.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();
}
if (ImGui.Button("Delete") && this.selectedLayout != null) {
this.plugin.config.Layouts.Remove(this.selectedLayout);
this.selectedLayout = Guid.Empty;
this.plugin.config.Save();
}
}
ImGui.Separator();
ImGui.Text("Import");
ImGui.InputText("Imported layout name", ref this.importName, 100);
foreach (HudSlot slot in Enum.GetValues(typeof(HudSlot))) {
string buttonName = $"{(int)slot + 1}##import";
if (ImGui.Button(buttonName) && this.importName != "") {
this.ImportSlot(slot, this.importName);
this.importName = "";
}
if (slot != HudSlot.Four) {
ImGui.SameLine();
}
}
ImGui.EndTabItem();
}
if (ImGui.BeginTabItem("Swaps")) {
bool 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();
string 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())) {
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();
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);
this.LayoutBox("Fishing", ref this.plugin.config.fishingLayout, player);
ImGui.Columns(1);
ImGui.EndTabItem();
}
ImGui.EndTabBar();
}
ImGui.End();
}
}
private void HelpMarker(string text) {
ImGui.TextDisabled("(?)");
if (ImGui.IsItemHovered()) {
ImGui.BeginTooltip();
ImGui.PushTextWrapPos(ImGui.GetFontSize() * 20f);
ImGui.TextUnformatted(text);
ImGui.PopTextWrapPos();
ImGui.EndTooltip();
}
}
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;
}
if (this.statuses.Update(player)) {
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();
if (this.plugin.config.SwapsEnabled) {
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();
if (this.plugin.config.SwapsEnabled) {
this.statuses.SetHudLayout(player, true);
}
}
}
ImGui.EndCombo();
}
ImGui.NextColumn();
}
public void ImportSlot(HudSlot slot, string name, bool save = true) {
this.plugin.config.Layouts[Guid.NewGuid()] = new Tuple<string, byte[]>(name, this.plugin.hud.ReadLayout(slot));
if (save) {
this.plugin.config.Save();
}
}
}
public class Statuses {
private readonly HudSwapPlugin plugin;
private readonly DalamudPluginInterface pi;
private readonly bool[] condition = new bool[ORDER.Length];
// Order: lowest to highest priority
// For conditions that require custom logic, use ConditionFlag.None
private static readonly ConditionFlag[] ORDER = {
ConditionFlag.Fishing,
ConditionFlag.Gathering,
ConditionFlag.Crafting,
ConditionFlag.BoundByDuty,
ConditionFlag.None, // weapon drawn
ConditionFlag.InCombat,
};
private delegate bool CustomCondition(HudSwapPlugin plugin, DalamudPluginInterface pi, PlayerCharacter player);
// Add handlers in the order that ConditionFlag.None flags appear in ORDER.
private static readonly CustomCondition[] CUSTOM = {
// weapon drawn
(plugin, pi, player) => (GetStatus(pi, player) & 4) > 0,
};
protected static byte GetStatus(DalamudPluginInterface pi, Actor actor) {
IntPtr statusPtr = pi.TargetModuleScanner.ResolveRelativeAddress(actor.Address, 0x1901);
return Marshal.ReadByte(statusPtr);
}
public Statuses(HudSwapPlugin plugin, DalamudPluginInterface pi) {
this.plugin = plugin;
this.pi = pi;
if (ORDER.Length != this.GetLayouts().Length) {
throw new ApplicationException("Statuses.ORDER is not the same length as the array returned by Statuses.GetLayouts()");
}
if (ORDER.Where(flag => flag == ConditionFlag.None).Count() != CUSTOM.Length) {
throw new ApplicationException("Statuses.CUSTOM does not have an amount of handlers equalling the amount of ConditionFlag.None in Statuses.ORDER");
}
}
private Guid[] GetLayouts() {
// These layouts must be in the same order as the flags in ORDER are defined
Guid[] layouts = {
this.plugin.config.fishingLayout,
this.plugin.config.gatheringLayout,
this.plugin.config.craftingLayout,
this.plugin.config.instanceLayout,
this.plugin.config.weaponDrawnLayout,
this.plugin.config.combatLayout,
};
return layouts;
}
public bool Update(PlayerCharacter player) {
if (player == null) {
return false;
}
int customs = 0;
bool[] old = (bool[])this.condition.Clone();
Condition condition = this.pi.ClientState.Condition;
bool anyChanged = false;
for (int i = 0; i < ORDER.Length; i++) {
ConditionFlag flag = ORDER[i];
if (flag == ConditionFlag.None) {
this.condition[i] = CUSTOM[customs].Invoke(this.plugin, this.pi, player);
customs += 1;
} else {
this.condition[i] = condition[flag];
}
anyChanged |= old[i] != this.condition[i];
}
return anyChanged;
}
public Guid CalculateCurrentHud() {
PlayerCharacter player = this.pi.ClientState.LocalPlayer;
if (player == null) {
return Guid.Empty;
}
this.Update(player);
Guid layout = this.plugin.config.defaultLayout;
Guid[] layouts = this.GetLayouts();
for (int i = 0; i < ORDER.Length; i++) {
Guid flagLayout = layouts[i];
if (this.condition[i] && flagLayout != Guid.Empty) {
layout = flagLayout;
}
}
return layout;
}
public void SetHudLayout(PlayerCharacter player, bool update = false) {
if (update && player != null) {
this.Update(player);
}
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(this.plugin.config.StagingSlot, entry.Item2);
this.plugin.hud.SelectSlot(this.plugin.config.StagingSlot, true);
}
}
}