feat: add window position handling

Also add /phud for swapping between saved layouts.

Currently only the map and chat box have saved positions, although the
implementation supports additional windows. There is a checkbox when
importing to turn on or off window position saving.
This commit is contained in:
Anna 2020-08-03 22:13:29 -04:00
parent 98931e1d3f
commit 8614c19835
9 changed files with 221 additions and 34 deletions

69
HudSwap/GameFunctions.cs Normal file
View File

@ -0,0 +1,69 @@
using Dalamud.Plugin;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace HudSwap {
public class GameFunctions {
private delegate IntPtr GetUIBaseDelegate();
private delegate IntPtr GetUIWindowDelegate(IntPtr uiBase, string uiName, int index);
private delegate void MoveWindowDelegate(IntPtr windowBase, short x, short y);
private readonly GetUIBaseDelegate getUIBase;
private readonly GetUIWindowDelegate getUIWindow;
private readonly MoveWindowDelegate moveWindow;
public GameFunctions(DalamudPluginInterface pi) {
if (pi == null) {
throw new ArgumentNullException(nameof(pi), "DalamudPluginInterface cannot be null");
}
IntPtr getUIBasePtr = pi.TargetModuleScanner.ScanText("E8 ?? ?? ?? ?? 41 b8 01 00 00 00 48 8d 15 ?? ?? ?? ?? 48 8b 48 20 e8 ?? ?? ?? ?? 48 8b cf");
IntPtr getUIWindowPtr = pi.TargetModuleScanner.ScanText("e8 ?? ?? ?? ?? 48 8b cf 48 89 87 ?? ?? 00 00 e8 ?? ?? ?? ?? 41 b8 01 00 00 00");
IntPtr moveWindowPtr = pi.TargetModuleScanner.ScanText("E8 ?? ?? ?? ?? 48 83 BB ?? ?? ?? ?? 00 74 ?? 48 8B 8B ?? ?? ?? ?? 48 85 C9 74 ?? E8 ?? ?? ?? ??");
if (getUIBasePtr == IntPtr.Zero || getUIWindowPtr == IntPtr.Zero || moveWindowPtr == IntPtr.Zero) {
throw new ApplicationException("could not get game functions");
}
this.getUIBase = Marshal.GetDelegateForFunctionPointer<GetUIBaseDelegate>(getUIBasePtr);
this.getUIWindow = Marshal.GetDelegateForFunctionPointer<GetUIWindowDelegate>(getUIWindowPtr);
this.moveWindow = Marshal.GetDelegateForFunctionPointer<MoveWindowDelegate>(moveWindowPtr);
}
private IntPtr GetUIBase() {
return this.getUIBase.Invoke();
}
private IntPtr GetUIWindow(string uiName, int index) {
IntPtr uiBase = this.GetUIBase();
IntPtr offset = Marshal.ReadIntPtr(uiBase, 0x20);
return this.getUIWindow.Invoke(offset, uiName, index);
}
public void MoveWindow(string uiName, short x, short y) {
IntPtr windowBase = this.GetUIWindow(uiName, 1);
if (windowBase == IntPtr.Zero) {
return;
}
this.moveWindow.Invoke(windowBase, x, y);
}
public Vector2<short> GetWindowPosition(string uiName) {
IntPtr windowBase = this.GetUIWindow(uiName, 1);
if (windowBase == IntPtr.Zero) {
return null;
}
short x = Marshal.ReadInt16(windowBase + 0x1bc);
short y = Marshal.ReadInt16(windowBase + 0x1bc + 2);
return new Vector2<short>(x, y);
}
}
}

View File

@ -8,3 +8,4 @@ using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Will be done eventually", Scope = "module")]
[assembly: SuppressMessage("Globalization", "CA1305", Scope = "module")]
[assembly: SuppressMessage("Globalization", "CA1304", Scope = "module")]
[assembly: SuppressMessage("Globalization", "CA1724", Scope = "module")]

View File

@ -1,8 +1,10 @@
using Dalamud.Plugin;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Runtime.InteropServices;
namespace HudSwap {
@ -98,6 +100,16 @@ namespace HudSwap {
Four = 3,
}
public class Vector2<T> {
public T X { get; private set; }
public T Y { get; private set; }
public Vector2(T x, T y) {
this.X = x;
this.Y = y;
}
}
[Serializable]
public class SharedLayout {
[JsonProperty]
@ -105,6 +117,8 @@ namespace HudSwap {
[NonSerialized]
private byte[] uncompressed = null;
public Dictionary<string, Vector2<short>> Positions { get; private set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "nah")]
public byte[] Layout() {
if (this.uncompressed != null) {
@ -132,15 +146,21 @@ namespace HudSwap {
// For JSON
}
public SharedLayout(byte[] layout) {
public SharedLayout(Layout layout) {
if (layout == null) {
throw new ArgumentNullException(nameof(layout), "Layout cannot be null");
}
using (MemoryStream compressed = new MemoryStream()) {
using (GZipStream gzip = new GZipStream(compressed, CompressionLevel.Optimal)) {
using (MemoryStream uncompressed = new MemoryStream(layout)) {
using (MemoryStream uncompressed = new MemoryStream(layout.Hud)) {
uncompressed.CopyTo(gzip);
}
}
this.compressed = compressed.ToArray();
}
this.Positions = layout.Positions;
}
}
}

View File

@ -70,6 +70,8 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="GameFunctions.cs" />
<Compile Include="Layout.cs" />
<Compile Include="PluginConfig.cs" />
<Compile Include="GlobalSuppressions.cs" />
<Compile Include="HUD.cs" />

19
HudSwap/Layout.cs Normal file
View File

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
namespace HudSwap {
[Serializable]
public class Layout {
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays")]
public byte[] Hud { get; private set; }
public Dictionary<string, Vector2<short>> Positions { get; private set; }
public string Name { get; private set; }
public Layout(string name, byte[] hud, Dictionary<string, Vector2<short>> positions) {
this.Name = name;
this.Hud = hud;
this.Positions = positions;
}
}
}

View File

@ -1,5 +1,8 @@
using Dalamud.Plugin;
using Dalamud.Game.Command;
using Dalamud.Plugin;
using System;
using System.Collections.Generic;
using System.Linq;
namespace HudSwap {
public class HudSwapPlugin : IDalamudPlugin {
@ -8,6 +11,7 @@ namespace HudSwap {
private DalamudPluginInterface pi;
private PluginUI ui;
public HUD Hud { get; private set; }
public GameFunctions GameFunctions { get; private set; }
public PluginConfig Config { get; private set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "nah")]
@ -23,12 +27,13 @@ namespace HudSwap {
this.ui = new PluginUI(this, this.pi);
this.Hud = new HUD(this.pi);
this.GameFunctions = new GameFunctions(this.pi);
if (this.Config.FirstRun) {
this.Config.FirstRun = false;
if (this.Config.Layouts.Count == 0) {
if (this.Config.Layouts2.Count == 0) {
foreach (HudSlot slot in Enum.GetValues(typeof(HudSlot))) {
this.ui.ImportSlot(slot, $"Auto-import {(int)slot + 1}", false);
this.ui.ImportSlot($"Auto-import {(int)slot + 1}", slot, false);
}
}
this.Config.Save();
@ -37,15 +42,19 @@ namespace HudSwap {
this.pi.UiBuilder.OnBuildUi += this.ui.Draw;
this.pi.UiBuilder.OnOpenConfigUi += this.ui.ConfigUI;
this.pi.CommandManager.AddHandler("/phudswap", new Dalamud.Game.Command.CommandInfo(OnCommand) {
this.pi.CommandManager.AddHandler("/phudswap", new CommandInfo(OnSettingsCommand) {
HelpMessage = "Open the HudSwap settings"
});
this.pi.CommandManager.AddHandler("/phud", new CommandInfo(OnSwapCommand) {
HelpMessage = "/phud <name> - Swap to HUD layout called <name>"
});
}
protected virtual void Dispose(bool all) {
this.pi.UiBuilder.OnBuildUi -= this.ui.Draw;
this.pi.UiBuilder.OnOpenConfigUi -= this.ui.ConfigUI;
this.pi.CommandManager.RemoveHandler("/phudswap");
this.pi.CommandManager.RemoveHandler("/phud");
}
public void Dispose() {
@ -53,8 +62,20 @@ namespace HudSwap {
GC.SuppressFinalize(this);
}
private void OnCommand(string command, string args) {
private void OnSettingsCommand(string command, string args) {
this.ui.SettingsVisible = true;
}
private void OnSwapCommand(string command, string args) {
KeyValuePair<Guid, Layout> entry = this.Config.Layouts2.FirstOrDefault(e => e.Value.Name == args);
if (entry.Equals(default(KeyValuePair<Guid, Layout>))) {
return;
}
Layout layout = entry.Value;
this.Hud.WriteLayout(this.Config.StagingSlot, layout.Hud);
this.Hud.SelectSlot(this.Config.StagingSlot, true);
}
}
}

View File

@ -1,7 +1,9 @@
using Dalamud.Configuration;
using Dalamud.Plugin;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace HudSwap {
[Serializable]
@ -14,6 +16,7 @@ namespace HudSwap {
public bool FirstRun { get; set; } = true;
public bool UnderstandsRisks { get; set; } = false;
public bool ImportPositions { get; set; } = false;
public bool SwapsEnabled { get; set; } = false;
public HudSlot StagingSlot { get; set; } = HudSlot.Four;
@ -43,7 +46,9 @@ namespace HudSwap {
public bool HighPriorityJobs { get; set; } = false;
public bool JobsCombatOnly { get; set; } = false;
[Obsolete("Use Layouts2 instead")]
public Dictionary<Guid, Tuple<string, byte[]>> Layouts { get; } = new Dictionary<Guid, Tuple<string, byte[]>>();
public Dictionary<Guid, Layout> Layouts2 { get; } = new Dictionary<Guid, Layout>();
public void Initialize(DalamudPluginInterface pluginInterface) {
this.pi = pluginInterface;
@ -91,6 +96,14 @@ namespace HudSwap {
this.StatusLayouts[Status.Roleplaying] = this.roleplayingLayout;
this.roleplayingLayout = Guid.Empty;
}
if (this.Layouts.Count != 0) {
foreach (KeyValuePair<Guid, Tuple<string, byte[]>> entry in this.Layouts) {
Layout layout = new Layout(entry.Value.Item1, entry.Value.Item2, new Dictionary<string, Vector2<short>>());
this.Layouts2.Add(entry.Key, layout);
}
this.Layouts.Clear();
}
#pragma warning restore 618
}
}

View File

@ -12,6 +12,15 @@ using System.Numerics;
namespace HudSwap {
public class PluginUI {
private static readonly string[] SAVED_WINDOWS = {
"AreaMap",
"ChatLog",
"ChatLogPanel_0",
"ChatLogPanel_1",
"ChatLogPanel_2",
"ChatLogPanel_3",
};
private readonly HudSwapPlugin plugin;
private readonly DalamudPluginInterface pi;
private readonly Statuses statuses;
@ -88,14 +97,14 @@ namespace HudSwap {
if (ImGui.BeginTabItem("Layouts")) {
ImGui.Text("Saved layouts");
if (this.plugin.Config.Layouts.Keys.Count == 0) {
if (this.plugin.Config.Layouts2.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)) {
foreach (KeyValuePair<Guid, Layout> entry in this.plugin.Config.Layouts2) {
if (ImGui.Selectable(entry.Value.Name, this.selectedLayout == entry.Key)) {
this.selectedLayout = entry.Key;
this.renameName = entry.Value.Item1;
this.renameName = entry.Value.Name;
}
}
ImGui.ListBoxFooter();
@ -105,14 +114,14 @@ namespace HudSwap {
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);
Layout layout = this.plugin.Config.Layouts2[this.selectedLayout];
this.plugin.Hud.WriteLayout(slot, layout.Hud.ToArray());
}
ImGui.SameLine();
}
if (ImGui.Button("Delete") && this.selectedLayout != null) {
this.plugin.Config.Layouts.Remove(this.selectedLayout);
this.plugin.Config.Layouts2.Remove(this.selectedLayout);
this.selectedLayout = Guid.Empty;
this.renameName = "";
this.plugin.Config.Save();
@ -120,8 +129,8 @@ namespace HudSwap {
ImGui.SameLine();
if (ImGui.Button("Copy to clipboard") && this.selectedLayout != null) {
if (this.plugin.Config.Layouts.TryGetValue(this.selectedLayout, out Tuple<string, byte[]> layout)) {
SharedLayout shared = new SharedLayout(layout.Item2);
if (this.plugin.Config.Layouts2.TryGetValue(this.selectedLayout, out Layout layout)) {
SharedLayout shared = new SharedLayout(layout);
string json = JsonConvert.SerializeObject(shared);
ImGui.SetClipboardText(json);
}
@ -130,8 +139,9 @@ namespace HudSwap {
ImGui.InputText("##rename-input", ref this.renameName, 100);
ImGui.SameLine();
if (ImGui.Button("Rename") && this.renameName.Length != 0 && this.selectedLayout != null) {
Tuple<string, byte[]> entry = this.plugin.Config.Layouts[this.selectedLayout]; ;
this.plugin.Config.Layouts[this.selectedLayout] = new Tuple<string, byte[]>(this.renameName, entry.Item2);
Layout layout = this.plugin.Config.Layouts2[this.selectedLayout];
Layout newLayout = new Layout(this.renameName, layout.Hud, layout.Positions);
this.plugin.Config.Layouts2[this.selectedLayout] = newLayout;
this.plugin.Config.Save();
}
}
@ -142,10 +152,18 @@ namespace HudSwap {
ImGui.InputText("Imported layout name", ref this.importName, 100);
bool 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.");
foreach (HudSlot slot in Enum.GetValues(typeof(HudSlot))) {
string buttonName = $"{(int)slot + 1}##import";
if (ImGui.Button(buttonName) && this.importName.Length != 0) {
this.ImportSlot(slot, this.importName);
this.ImportSlot(this.importName, slot);
this.importName = "";
}
ImGui.SameLine();
@ -162,7 +180,7 @@ namespace HudSwap {
if (shared != null) {
byte[] layout = shared.Layout();
if (layout != null) {
this.Import(layout, this.importName);
this.Import(this.importName, layout, shared.Positions);
this.importName = "";
}
}
@ -198,8 +216,8 @@ namespace HudSwap {
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)) {
foreach (KeyValuePair<Guid, Layout> entry in this.plugin.Config.Layouts2) {
if (ImGui.Selectable(entry.Value.Name)) {
this.plugin.Config.DefaultLayout = entry.Key;
this.plugin.Config.Save();
}
@ -317,8 +335,8 @@ namespace HudSwap {
}
private string LayoutNameOrDefault(Guid key) {
if (this.plugin.Config.Layouts.TryGetValue(key, out Tuple<string, byte[]> tuple)) {
return tuple.Item1;
if (this.plugin.Config.Layouts2.TryGetValue(key, out Layout layout)) {
return layout.Name;
} else {
return "";
}
@ -351,8 +369,8 @@ namespace HudSwap {
updated = true;
}
ImGui.Separator();
foreach (KeyValuePair<Guid, Tuple<string, byte[]>> entry in this.plugin.Config.Layouts) {
if (ImGui.Selectable(entry.Value.Item1)) {
foreach (KeyValuePair<Guid, Layout> entry in this.plugin.Config.Layouts2) {
if (ImGui.Selectable(entry.Value.Name)) {
updated = true;
newLayout = entry.Key;
}
@ -364,12 +382,31 @@ namespace HudSwap {
return updated;
}
public void ImportSlot(HudSlot slot, string name, bool save = true) {
this.Import(this.plugin.Hud.ReadLayout(slot), name, save);
private Dictionary<string, Vector2<short>> GetPositions() {
Dictionary<string, Vector2<short>> positions = new Dictionary<string, Vector2<short>>();
foreach (string name in SAVED_WINDOWS) {
Vector2<short> pos = this.plugin.GameFunctions.GetWindowPosition(name);
if (pos != null) {
positions[name] = pos;
}
}
return positions;
}
public void Import(byte[] layout, string name, bool save = true) {
this.plugin.Config.Layouts[Guid.NewGuid()] = new Tuple<string, byte[]>(name, layout);
public void ImportSlot(string name, HudSlot slot, bool save = true) {
Dictionary<string, Vector2<short>> positions;
if (this.plugin.Config.ImportPositions) {
positions = this.GetPositions();
} else {
positions = new Dictionary<string, Vector2<short>>();
}
this.Import(name, this.plugin.Hud.ReadLayout(slot), positions, save);
}
public void Import(string name, byte[] layout, Dictionary<string, Vector2<short>> positions, bool save = true) {
this.plugin.Config.Layouts2[Guid.NewGuid()] = new Layout(name, layout, positions);
if (save) {
this.plugin.Config.Save();
}

View File

@ -3,6 +3,7 @@ using Dalamud.Game.ClientState.Actors.Types;
using Dalamud.Plugin;
using Lumina.Excel.GeneratedSheets;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
// TODO: Zone swaps?
@ -104,15 +105,19 @@ namespace HudSwap {
this.Update(player);
}
Guid layout = this.CalculateCurrentHud();
if (layout == Guid.Empty) {
Guid layoutId = this.CalculateCurrentHud();
if (layoutId == Guid.Empty) {
return; // FIXME: do something better
}
if (!this.plugin.Config.Layouts.TryGetValue(layout, out Tuple<string, byte[]> entry)) {
if (!this.plugin.Config.Layouts2.TryGetValue(layoutId, out Layout layout)) {
return; // FIXME: do something better
}
this.plugin.Hud.WriteLayout(this.plugin.Config.StagingSlot, entry.Item2);
this.plugin.Hud.WriteLayout(this.plugin.Config.StagingSlot, layout.Hud);
this.plugin.Hud.SelectSlot(this.plugin.Config.StagingSlot, true);
foreach (KeyValuePair<string, Vector2<short>> entry in layout.Positions) {
this.plugin.GameFunctions.MoveWindow(entry.Key, entry.Value.X, entry.Value.Y);
}
}
}