using System; using System.Numerics; using System.Runtime.InteropServices; using Dalamud.Game.Gui.Addons; using Dalamud.Game.Gui.FlyText; using Dalamud.Game.Gui.PartyFinder; using Dalamud.Game.Gui.Toast; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Hooking; using Dalamud.Interface; using Dalamud.Utility; using ImGuiNET; using Serilog; namespace Dalamud.Game.Gui { /// /// A class handling many aspects of the in-game UI. /// public sealed class GameGui : IDisposable { private readonly Dalamud dalamud; private readonly GameGuiAddressResolver address; private readonly GetMatrixSingletonDelegate getMatrixSingleton; private readonly GetUIObjectDelegate getUIObject; private readonly ScreenToWorldNativeDelegate screenToWorldNative; private readonly GetUIObjectByNameDelegate getUIObjectByName; private readonly GetUiModuleDelegate getUiModule; private readonly GetAgentModuleDelegate getAgentModule; private readonly Hook setGlobalBgmHook; private readonly Hook handleItemHoverHook; private readonly Hook handleItemOutHook; private readonly Hook handleActionHoverHook; private readonly Hook handleActionOutHook; private readonly Hook handleImmHook; private readonly Hook toggleUiHideHook; private GetUIMapObjectDelegate getUIMapObject; private OpenMapWithFlagDelegate openMapWithFlag; /// /// Initializes a new instance of the class. /// This class is responsible for many aspects of interacting with the native game UI. /// /// The base address of the native GuiManager class. /// The SigScanner instance. /// The Dalamud instance. internal GameGui(IntPtr baseAddress, SigScanner scanner, Dalamud dalamud) { this.dalamud = dalamud; this.address = new GameGuiAddressResolver(baseAddress); this.address.Setup(scanner); Log.Verbose("===== G A M E G U I ====="); Log.Verbose($"GameGuiManager address 0x{this.address.BaseAddress.ToInt64():X}"); Log.Verbose($"SetGlobalBgm address 0x{this.address.SetGlobalBgm.ToInt64():X}"); Log.Verbose($"HandleItemHover address 0x{this.address.HandleItemHover.ToInt64():X}"); Log.Verbose($"HandleItemOut address 0x{this.address.HandleItemOut.ToInt64():X}"); Log.Verbose($"HandleImm address 0x{this.address.HandleImm.ToInt64():X}"); Log.Verbose($"GetUIObject address 0x{this.address.GetUIObject.ToInt64():X}"); Log.Verbose($"GetAgentModule address 0x{this.address.GetAgentModule.ToInt64():X}"); this.Chat = new ChatGui(this.address.ChatManager, scanner, dalamud); this.PartyFinder = new PartyFinderGui(scanner, dalamud); this.Toast = new ToastGui(scanner, dalamud); this.FlyText = new FlyTextGui(scanner, dalamud); this.setGlobalBgmHook = new Hook(this.address.SetGlobalBgm, this.HandleSetGlobalBgmDetour); this.handleItemHoverHook = new Hook(this.address.HandleItemHover, this.HandleItemHoverDetour); this.handleItemOutHook = new Hook(this.address.HandleItemOut, this.HandleItemOutDetour); this.handleActionHoverHook = new Hook(this.address.HandleActionHover, this.HandleActionHoverDetour); this.handleActionOutHook = new Hook(this.address.HandleActionOut, this.HandleActionOutDetour); this.handleImmHook = new Hook(this.address.HandleImm, this.HandleImmDetour); this.getUIObject = Marshal.GetDelegateForFunctionPointer(this.address.GetUIObject); this.getMatrixSingleton = Marshal.GetDelegateForFunctionPointer(this.address.GetMatrixSingleton); this.screenToWorldNative = Marshal.GetDelegateForFunctionPointer(this.address.ScreenToWorld); this.toggleUiHideHook = new Hook(this.address.ToggleUiHide, this.ToggleUiHideDetour); this.GetBaseUIObject = Marshal.GetDelegateForFunctionPointer(this.address.GetBaseUIObject); this.getUIObjectByName = Marshal.GetDelegateForFunctionPointer(this.address.GetUIObjectByName); this.getUiModule = Marshal.GetDelegateForFunctionPointer(this.address.GetUIModule); this.getAgentModule = Marshal.GetDelegateForFunctionPointer(this.address.GetAgentModule); } // Marshaled delegates /// /// The delegate type of the native method that gets the Client::UI::UIModule address. /// /// The Client::UI::UIModule address. [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate IntPtr GetBaseUIObjectDelegate(); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate IntPtr GetMatrixSingletonDelegate(); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate IntPtr GetUIObjectDelegate(); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private unsafe delegate bool ScreenToWorldNativeDelegate(float* camPos, float* clipPos, float rayDistance, float* worldPos, int* unknown); [UnmanagedFunctionPointer(CallingConvention.ThisCall, CharSet = CharSet.Ansi)] private delegate IntPtr GetUIObjectByNameDelegate(IntPtr thisPtr, string uiName, int index); private delegate IntPtr GetUiModuleDelegate(IntPtr basePtr); private delegate IntPtr GetAgentModuleDelegate(IntPtr uiModule); // Hooked delegates [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr GetUIMapObjectDelegate(IntPtr uiObject); [UnmanagedFunctionPointer(CallingConvention.ThisCall, CharSet = CharSet.Ansi)] private delegate bool OpenMapWithFlagDelegate(IntPtr uiMapObject, string flag); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr SetGlobalBgmDelegate(ushort bgmKey, byte a2, uint a3, uint a4, uint a5, byte a6); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr HandleItemHoverDelegate(IntPtr hoverState, IntPtr a2, IntPtr a3, ulong a4); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr HandleItemOutDelegate(IntPtr hoverState, IntPtr a2, IntPtr a3, ulong a4); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void HandleActionHoverDelegate(IntPtr hoverState, HoverActionKind a2, uint a3, int a4, byte a5); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr HandleActionOutDelegate(IntPtr agentActionDetail, IntPtr a2, IntPtr a3, int a4); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate char HandleImmDelegate(IntPtr framework, char a2, byte a3); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr ToggleUiHideDelegate(IntPtr thisPtr, byte unknownByte); /// /// Event which is fired when the game UI hiding is toggled. /// public event EventHandler OnUiHideToggled; /// /// Gets a callable delegate for the GetBaseUIObject game method. /// /// The Client::UI::UIModule address. public GetBaseUIObjectDelegate GetBaseUIObject { get; } /// /// Gets the instance. /// public ChatGui Chat { get; private set; } /// /// Gets the instance. /// public PartyFinderGui PartyFinder { get; private set; } /// /// Gets the instance. /// public ToastGui Toast { get; private set; } /// /// Gets the instance. /// public FlyTextGui FlyText { get; private set; } /// /// Gets a value indicating whether the game UI is hidden. /// public bool GameUiHidden { get; private set; } /// /// Gets or sets the item ID that is currently hovered by the player. 0 when no item is hovered. /// If > 1.000.000, subtract 1.000.000 and treat it as HQ. /// public ulong HoveredItem { get; set; } /// /// Gets the action ID that is current hovered by the player. 0 when no action is hovered. /// public HoveredAction HoveredAction { get; } = new HoveredAction(); /// /// Gets or sets the event that is fired when the currently hovered item changes. /// public EventHandler HoveredItemChanged { get; set; } /// /// Gets or sets the event that is fired when the currently hovered action changes. /// public EventHandler HoveredActionChanged { get; set; } /// /// Opens the in-game map with a flag on the location of the parameter. /// /// Link to the map to be opened. /// True if there were no errors and it could open the map. public bool OpenMapWithMapLink(MapLinkPayload mapLink) { var uiObjectPtr = this.getUIObject(); if (uiObjectPtr.Equals(IntPtr.Zero)) { Log.Error("OpenMapWithMapLink: Null pointer returned from getUIObject()"); return false; } this.getUIMapObject = this.address.GetVirtualFunction(uiObjectPtr, 0, 8); var uiMapObjectPtr = this.getUIMapObject(uiObjectPtr); if (uiMapObjectPtr.Equals(IntPtr.Zero)) { Log.Error("OpenMapWithMapLink: Null pointer returned from GetUIMapObject()"); return false; } this.openMapWithFlag = this.address.GetVirtualFunction(uiMapObjectPtr, 0, 63); var mapLinkString = mapLink.DataString; Log.Debug($"OpenMapWithMapLink: Opening Map Link: {mapLinkString}"); return this.openMapWithFlag(uiMapObjectPtr, mapLinkString); } /// /// Converts in-world coordinates to screen coordinates (upper left corner origin). /// /// Coordinates in the world. /// Converted coordinates. /// True if worldPos corresponds to a position in front of the camera. public bool WorldToScreen(SharpDX.Vector3 worldPos, out SharpDX.Vector2 screenPos) { // Get base object with matrices var matrixSingleton = this.getMatrixSingleton(); // Read current ViewProjectionMatrix plus game window size var viewProjectionMatrix = default(SharpDX.Matrix); float width, height; var windowPos = ImGuiHelpers.MainViewport.Pos; unsafe { var rawMatrix = (float*)(matrixSingleton + 0x1b4).ToPointer(); for (var i = 0; i < 16; i++, rawMatrix++) viewProjectionMatrix[i] = *rawMatrix; width = *rawMatrix; height = *(rawMatrix + 1); } SharpDX.Vector3.Transform(ref worldPos, ref viewProjectionMatrix, out SharpDX.Vector3 pCoords); screenPos = new SharpDX.Vector2(pCoords.X / pCoords.Z, pCoords.Y / pCoords.Z); screenPos.X = (0.5f * width * (screenPos.X + 1f)) + windowPos.X; screenPos.Y = (0.5f * height * (1f - screenPos.Y)) + windowPos.Y; return pCoords.Z > 0 && screenPos.X > windowPos.X && screenPos.X < windowPos.X + width && screenPos.Y > windowPos.Y && screenPos.Y < windowPos.Y + height; } /// /// Converts in-world coordinates to screen coordinates (upper left corner origin). /// /// Coordinates in the world. /// Converted coordinates. /// True if worldPos corresponds to a position in front of the camera. /// /// This overload requires a conversion to SharpDX vectors, however the penalty should be negligible. /// public bool WorldToScreen(Vector3 worldPos, out Vector2 screenPos) { var result = this.WorldToScreen(worldPos.ToSharpDX(), out var sharpScreenPos); screenPos = sharpScreenPos.ToSystem(); return result; } /// /// Converts screen coordinates to in-world coordinates via raycasting. /// /// Screen coordinates. /// Converted coordinates. /// How far to search for a collision. /// True if successful. On false, worldPos's contents are undefined. public bool ScreenToWorld(SharpDX.Vector2 screenPos, out SharpDX.Vector3 worldPos, float rayDistance = 100000.0f) { // The game is only visible in the main viewport, so if the cursor is outside // of the game window, do not bother calculating anything var windowPos = ImGuiHelpers.MainViewport.Pos; var windowSize = ImGuiHelpers.MainViewport.Size; if (screenPos.X < windowPos.X || screenPos.X > windowPos.X + windowSize.X || screenPos.Y < windowPos.Y || screenPos.Y > windowPos.Y + windowSize.Y) { worldPos = default; return false; } // Get base object with matrices var matrixSingleton = this.getMatrixSingleton(); // Read current ViewProjectionMatrix plus game window size var viewProjectionMatrix = default(SharpDX.Matrix); float width, height; unsafe { var rawMatrix = (float*)(matrixSingleton + 0x1b4).ToPointer(); for (var i = 0; i < 16; i++, rawMatrix++) viewProjectionMatrix[i] = *rawMatrix; width = *rawMatrix; height = *(rawMatrix + 1); } viewProjectionMatrix.Invert(); var localScreenPos = new SharpDX.Vector2(screenPos.X - windowPos.X, screenPos.Y - windowPos.Y); var screenPos3D = new SharpDX.Vector3 { X = (localScreenPos.X / width * 2.0f) - 1.0f, Y = -((localScreenPos.Y / height * 2.0f) - 1.0f), Z = 0, }; SharpDX.Vector3.TransformCoordinate(ref screenPos3D, ref viewProjectionMatrix, out var camPos); screenPos3D.Z = 1; SharpDX.Vector3.TransformCoordinate(ref screenPos3D, ref viewProjectionMatrix, out var camPosOne); var clipPos = camPosOne - camPos; clipPos.Normalize(); bool isSuccess; unsafe { var camPosArray = camPos.ToArray(); var clipPosArray = clipPos.ToArray(); // This array is larger than necessary because it contains more info than we currently use var worldPosArray = stackalloc float[32]; // Theory: this is some kind of flag on what type of things the ray collides with var unknown = stackalloc int[3] { 0x4000, 0x4000, 0x0, }; fixed (float* pCamPos = camPosArray) { fixed (float* pClipPos = clipPosArray) { isSuccess = this.screenToWorldNative(pCamPos, pClipPos, rayDistance, worldPosArray, unknown); } } worldPos = new SharpDX.Vector3 { X = worldPosArray[0], Y = worldPosArray[1], Z = worldPosArray[2], }; } return isSuccess; } /// /// Converts screen coordinates to in-world coordinates via raycasting. /// /// Screen coordinates. /// Converted coordinates. /// How far to search for a collision. /// True if successful. On false, worldPos's contents are undefined. /// /// This overload requires a conversion to SharpDX vectors, however the penalty should be negligible. /// public bool ScreenToWorld(Vector2 screenPos, out Vector3 worldPos, float rayDistance = 100000.0f) { var result = this.ScreenToWorld(screenPos.ToSharpDX(), out var sharpworldPos); worldPos = sharpworldPos.ToSystem(); return result; } /// /// Gets a pointer to the game's UI module. /// /// IntPtr pointing to UI module. public IntPtr GetUIModule() => this.getUiModule(this.dalamud.Framework.Address.BaseAddress); /// /// Gets the pointer to the UI Object with the given name and index. /// /// Name of UI to find. /// Index of UI to find (1-indexed). /// IntPtr.Zero if unable to find UI, otherwise IntPtr pointing to the start of the UI Object. public IntPtr GetUiObjectByName(string name, int index) { var baseUi = this.GetBaseUIObject(); if (baseUi == IntPtr.Zero) return IntPtr.Zero; var baseUiProperties = Marshal.ReadIntPtr(baseUi, 0x20); if (baseUiProperties == IntPtr.Zero) return IntPtr.Zero; return this.getUIObjectByName(baseUiProperties, name, index); } /// /// Gets an Addon by it's internal name. /// /// The addon name. /// The index of the addon, starting at 1. /// The native memory representation of the addon, if it exists. public Addon GetAddonByName(string name, int index) { var address = this.GetUiObjectByName(name, index); if (address == IntPtr.Zero) return null; return new Addon(address); } /// /// Find the agent associated with an addon, if possible. /// /// The addon name. /// A pointer to the agent interface. public IntPtr FindAgentInterface(string addonName) { var addon = this.dalamud.Framework.Gui.GetUiObjectByName(addonName, 1); return this.FindAgentInterface(addon); } /// /// Find the agent associated with an addon, if possible. /// /// The addon address. /// A pointer to the agent interface. public IntPtr FindAgentInterface(IntPtr addon) { if (addon == IntPtr.Zero) return IntPtr.Zero; var uiModule = this.dalamud.Framework.Gui.GetUIModule(); if (uiModule == IntPtr.Zero) { return IntPtr.Zero; } var agentModule = this.getAgentModule(uiModule); if (agentModule == IntPtr.Zero) { return IntPtr.Zero; } var id = Marshal.ReadInt16(addon, 0x1CE); if (id == 0) id = Marshal.ReadInt16(addon, 0x1CC); if (id == 0) return IntPtr.Zero; for (var i = 0; i < 380; i++) { var agent = Marshal.ReadIntPtr(agentModule, 0x20 + (i * 8)); if (agent == IntPtr.Zero) continue; if (Marshal.ReadInt32(agent, 0x20) == id) return agent; } return IntPtr.Zero; } /// /// Set the current background music. /// /// The background music key. public void SetBgm(ushort bgmKey) => this.setGlobalBgmHook.Original(bgmKey, 0, 0, 0, 0, 0); /// /// Enables the hooks and submodules of this module. /// public void Enable() { this.Chat.Enable(); this.Toast.Enable(); this.FlyText.Enable(); this.PartyFinder.Enable(); this.setGlobalBgmHook.Enable(); this.handleItemHoverHook.Enable(); this.handleItemOutHook.Enable(); this.handleImmHook.Enable(); this.toggleUiHideHook.Enable(); this.handleActionHoverHook.Enable(); this.handleActionOutHook.Enable(); } /// /// Disables the hooks and submodules of this module. /// public void Dispose() { this.Chat.Dispose(); this.Toast.Dispose(); this.FlyText.Dispose(); this.PartyFinder.Dispose(); this.setGlobalBgmHook.Dispose(); this.handleItemHoverHook.Dispose(); this.handleItemOutHook.Dispose(); this.handleImmHook.Dispose(); this.toggleUiHideHook.Dispose(); this.handleActionHoverHook.Dispose(); this.handleActionOutHook.Dispose(); } private IntPtr HandleSetGlobalBgmDetour(ushort bgmKey, byte a2, uint a3, uint a4, uint a5, byte a6) { var retVal = this.setGlobalBgmHook.Original(bgmKey, a2, a3, a4, a5, a6); Log.Verbose("SetGlobalBgm: {0} {1} {2} {3} {4} {5} -> {6}", bgmKey, a2, a3, a4, a5, a6, retVal); return retVal; } private IntPtr HandleItemHoverDetour(IntPtr hoverState, IntPtr a2, IntPtr a3, ulong a4) { var retVal = this.handleItemHoverHook.Original(hoverState, a2, a3, a4); if (retVal.ToInt64() == 22) { var itemId = (ulong)Marshal.ReadInt32(hoverState, 0x138); this.HoveredItem = itemId; try { this.HoveredItemChanged?.Invoke(this, itemId); } catch (Exception e) { Log.Error(e, "Could not dispatch HoveredItemChanged event."); } Log.Verbose("HoverItemId:{0} this:{1}", itemId, hoverState.ToInt64().ToString("X")); } return retVal; } private IntPtr HandleItemOutDetour(IntPtr hoverState, IntPtr a2, IntPtr a3, ulong a4) { var retVal = this.handleItemOutHook.Original(hoverState, a2, a3, a4); if (a3 != IntPtr.Zero && a4 == 1) { var a3Val = Marshal.ReadByte(a3, 0x8); if (a3Val == 255) { this.HoveredItem = 0ul; try { this.HoveredItemChanged?.Invoke(this, 0ul); } catch (Exception e) { Log.Error(e, "Could not dispatch HoveredItemChanged event."); } Log.Verbose("HoverItemId: 0"); } } return retVal; } private void HandleActionHoverDetour(IntPtr hoverState, HoverActionKind actionKind, uint actionId, int a4, byte a5) { this.handleActionHoverHook.Original(hoverState, actionKind, actionId, a4, a5); this.HoveredAction.ActionKind = actionKind; this.HoveredAction.BaseActionID = actionId; this.HoveredAction.ActionID = (uint)Marshal.ReadInt32(hoverState, 0x3C); try { this.HoveredActionChanged?.Invoke(this, this.HoveredAction); } catch (Exception e) { Log.Error(e, "Could not dispatch HoveredItemChanged event."); } Log.Verbose("HoverActionId: {0}/{1} this:{2}", actionKind, actionId, hoverState.ToInt64().ToString("X")); } private IntPtr HandleActionOutDetour(IntPtr agentActionDetail, IntPtr a2, IntPtr a3, int a4) { var retVal = this.handleActionOutHook.Original(agentActionDetail, a2, a3, a4); if (a3 != IntPtr.Zero && a4 == 1) { var a3Val = Marshal.ReadByte(a3, 0x8); if (a3Val == 255) { this.HoveredAction.ActionKind = HoverActionKind.None; this.HoveredAction.BaseActionID = 0; this.HoveredAction.ActionID = 0; try { this.HoveredActionChanged?.Invoke(this, this.HoveredAction); } catch (Exception e) { Log.Error(e, "Could not dispatch HoveredActionChanged event."); } Log.Verbose("HoverActionId: 0"); } } return retVal; } private IntPtr ToggleUiHideDetour(IntPtr thisPtr, byte unknownByte) { this.GameUiHidden = !this.GameUiHidden; try { this.OnUiHideToggled?.Invoke(this, this.GameUiHidden); } catch (Exception ex) { Log.Error(ex, "Error on OnUiHideToggled event dispatch"); } Log.Debug("UiHide toggled: {0}", this.GameUiHidden); return this.toggleUiHideHook.Original(thisPtr, unknownByte); } private char HandleImmDetour(IntPtr framework, char a2, byte a3) { var result = this.handleImmHook.Original(framework, a2, a3); return ImGui.GetIO().WantTextInput ? (char)0 : result; } } }