using Dalamud.Plugin; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Runtime.InteropServices; namespace HudSwap { public class HUD { private const int LAYOUT_SIZE = 0xb40; // 5.4 private const int SLOT_OFFSET = 0x59e8; // 5.4 private delegate IntPtr GetFilePointerDelegate(byte index); private delegate uint SetHudLayoutDelegate(IntPtr filePtr, uint hudLayout, byte unk0, byte unk1); private readonly GetFilePointerDelegate _getFilePointer; private readonly SetHudLayoutDelegate _setHudLayout; private readonly DalamudPluginInterface pi; public HUD(DalamudPluginInterface pi) { this.pi = pi ?? throw new ArgumentNullException(nameof(pi), "DalamudPluginInterface cannot be null"); IntPtr getFilePointerPtr = this.pi.TargetModuleScanner.ScanText("E8 ?? ?? ?? ?? 48 85 C0 74 14 83 7B 44 00"); IntPtr setHudLayoutPtr = this.pi.TargetModuleScanner.ScanText("E8 ?? ?? ?? ?? 33 C0 EB 15"); if (getFilePointerPtr != IntPtr.Zero) { this._getFilePointer = Marshal.GetDelegateForFunctionPointer(getFilePointerPtr); } if (setHudLayoutPtr != IntPtr.Zero) { this._setHudLayout = Marshal.GetDelegateForFunctionPointer(setHudLayoutPtr); } } private IntPtr GetFilePointer(byte index) { return this._getFilePointer.Invoke(index); } public uint SelectSlot(HudSlot slot, bool force = false) { IntPtr file = this.GetFilePointer(0); // change the current slot so the game lets us pick one that's currently in use if (!force) { goto Return; } IntPtr currentSlotPtr = this.GetDataPointer() + SLOT_OFFSET; // read the current slot uint currentSlot = (uint)Marshal.ReadInt32(currentSlotPtr); // change it to a different slot if (currentSlot == (uint)slot) { if (currentSlot < 3) { currentSlot += 1; } else { currentSlot = 0; } // back up this different slot byte[] backup = this.ReadLayout((HudSlot)currentSlot); // change the current slot in memory Marshal.WriteInt32(currentSlotPtr, (int)currentSlot); // ask the game to change slot to our desired slot // for some reason, this overwrites the current slot, so this is why we back up uint res = this._setHudLayout.Invoke(file, (uint)slot, 0, 1); // restore the backup this.WriteLayout((HudSlot)currentSlot, backup); return res; } Return: 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 HudSlot GetActiveHudSlot() { int slotVal = Marshal.ReadInt32(this.GetDataPointer() + SLOT_OFFSET); if (!Enum.IsDefined(typeof(HudSlot), slotVal)) { throw new IOException($"invalid hud slot in FFXIV memory of ${slotVal}"); } return (HudSlot)slotVal; } 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 == null) { throw new ArgumentNullException(nameof(layout), "layout cannot be null"); } 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); var currentSlot = this.GetActiveHudSlot(); if (currentSlot == slot) { this.SelectSlot(currentSlot, true); } } public void WriteLayout(byte[] layout) => WriteLayout(this.GetActiveHudSlot(), layout); } public enum HudSlot { One = 0, Two = 1, Three = 2, Four = 3, } public class Vector2 { 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] private readonly byte[] compressed; [NonSerialized] private byte[] uncompressed = null; public Dictionary> 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) { return this.uncompressed; } try { using (MemoryStream compressed = new MemoryStream(this.compressed)) { using (GZipStream gzip = new GZipStream(compressed, CompressionMode.Decompress)) { using (MemoryStream uncompressed = new MemoryStream()) { gzip.CopyTo(uncompressed); this.uncompressed = uncompressed.ToArray(); } } } } catch (Exception) { return null; } return this.uncompressed; } [JsonConstructor] private SharedLayout() { // For JSON } 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.Hud)) { uncompressed.CopyTo(gzip); } } this.compressed = compressed.ToArray(); } this.Positions = layout.Positions; } } }