using System; using System.Runtime.InteropServices; using Dalamud.Game.Chat.SeStringHandling.Payloads; using Dalamud.Hooking; using Serilog; using SharpDX; namespace Dalamud.Game.Internal.Gui { public sealed class GameGui : IDisposable { private GameGuiAddressResolver Address { get; } public ChatGui Chat { get; private set; } [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr SetGlobalBgmDelegate(UInt16 bgmKey, byte a2, UInt32 a3, UInt32 a4, UInt32 a5, byte a6); private readonly Hook setGlobalBgmHook; [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr HandleItemHoverDelegate(IntPtr hoverState, IntPtr a2, IntPtr a3, ulong a4); private readonly Hook handleItemHoverHook; [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr HandleItemOutDelegate(IntPtr hoverState, IntPtr a2, IntPtr a3, ulong a4); private readonly Hook handleItemOutHook; [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void HandleActionHoverDelegate(IntPtr hoverState, HoverActionKind a2, uint a3, int a4, byte a5); private readonly Hook handleActionHoverHook; [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr HandleActionOutDelegate(IntPtr agentActionDetail, IntPtr a2, IntPtr a3, int a4); private Hook handleActionOutHook; [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate IntPtr GetUIObjectDelegate(); private readonly GetUIObjectDelegate getUIObject; [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr GetUIMapObjectDelegate(IntPtr UIObject); private GetUIMapObjectDelegate getUIMapObject; [UnmanagedFunctionPointer(CallingConvention.ThisCall, CharSet = CharSet.Ansi)] private delegate bool OpenMapWithFlagDelegate(IntPtr UIMapObject, string flag); private OpenMapWithFlagDelegate openMapWithFlag; [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate IntPtr GetMatrixSingletonDelegate(); internal readonly GetMatrixSingletonDelegate getMatrixSingleton; [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private unsafe delegate bool ScreenToWorldNativeDelegate( float *camPos, float *clipPos, float rayDistance, float *worldPos, int *unknown); private readonly ScreenToWorldNativeDelegate screenToWorldNative; [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr ToggleUiHideDelegate(IntPtr thisPtr, byte unknownByte); private readonly Hook toggleUiHideHook; // Return a Client::UI::UIModule [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate IntPtr GetBaseUIObjectDelegate(); public readonly GetBaseUIObjectDelegate GetBaseUIObject; [UnmanagedFunctionPointer(CallingConvention.ThisCall, CharSet = CharSet.Ansi)] private delegate IntPtr GetUIObjectByNameDelegate(IntPtr thisPtr, string uiName, int index); private readonly GetUIObjectByNameDelegate getUIObjectByName; public bool GameUiHidden { get; private set; } /// /// Event which is fired when the game UI hiding is toggled. /// public event EventHandler OnUiHideToggled; /// /// 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; } /// /// The action ID that is current hovered by the player. 0 when no action is hovered. /// public HoveredAction HoveredAction { get; } = new HoveredAction(); /// /// Event that is fired when the currently hovered item changes. /// public EventHandler HoveredItemChanged { get; set; } /// /// Event that is fired when the currently hovered action changes. /// public EventHandler HoveredActionChanged { get; set; } public GameGui(IntPtr baseAddress, SigScanner scanner, Dalamud dalamud) { Address = new GameGuiAddressResolver(baseAddress); Address.Setup(scanner); Log.Verbose("===== G A M E G U I ====="); Log.Verbose("GameGuiManager address {Address}", Address.BaseAddress); Log.Verbose("SetGlobalBgm address {Address}", Address.SetGlobalBgm); Log.Verbose("HandleItemHover address {Address}", Address.HandleItemHover); Log.Verbose("HandleItemOut address {Address}", Address.HandleItemOut); Log.Verbose("GetUIObject address {Address}", Address.GetUIObject); Chat = new ChatGui(Address.ChatManager, scanner, dalamud); this.setGlobalBgmHook = new Hook(Address.SetGlobalBgm, new SetGlobalBgmDelegate(HandleSetGlobalBgmDetour), this); this.handleItemHoverHook = new Hook(Address.HandleItemHover, new HandleItemHoverDelegate(HandleItemHoverDetour), this); this.handleItemOutHook = new Hook(Address.HandleItemOut, new HandleItemOutDelegate(HandleItemOutDetour), this); this.handleActionHoverHook = new Hook(Address.HandleActionHover, new HandleActionHoverDelegate(HandleActionHoverDetour), this); this.handleActionOutHook = new Hook(Address.HandleActionOut, new HandleActionOutDelegate(HandleActionOutDetour), this); this.getUIObject = Marshal.GetDelegateForFunctionPointer(Address.GetUIObject); this.getMatrixSingleton = Marshal.GetDelegateForFunctionPointer(Address.GetMatrixSingleton); this.screenToWorldNative = Marshal.GetDelegateForFunctionPointer(Address.ScreenToWorld); this.toggleUiHideHook = new Hook(Address.ToggleUiHide, new ToggleUiHideDelegate(ToggleUiHideDetour), this); this.GetBaseUIObject = Marshal.GetDelegateForFunctionPointer(Address.GetBaseUIObject); this.getUIObjectByName = Marshal.GetDelegateForFunctionPointer(Address.GetUIObjectByName); } private IntPtr HandleSetGlobalBgmDetour(UInt16 bgmKey, byte a2, UInt32 a3, UInt32 a4, UInt32 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 { 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 { 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) { handleActionHoverHook.Original(hoverState, actionKind, actionId, a4, a5); HoveredAction.ActionKind = actionKind; HoveredAction.BaseActionID = actionId; HoveredAction.ActionID = (uint) Marshal.ReadInt32(hoverState, 0x3C); try { 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 = 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; HoveredAction.BaseActionID = 0; HoveredAction.ActionID = 0; try { HoveredActionChanged?.Invoke(this, this.HoveredAction); } catch (Exception e) { Log.Error(e, "Could not dispatch HoveredActionChanged event."); } Log.Verbose("HoverActionId: 0"); } } return retVal; } /// /// 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 = 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 = 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(Vector3 worldPos, out Vector2 screenPos) { // Get base object with matrices var matrixSingleton = this.getMatrixSingleton(); // Read current ViewProjectionMatrix plus game window size var viewProjectionMatrix = new 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); } Vector3.Transform( ref worldPos, ref viewProjectionMatrix, out Vector3 pCoords); screenPos = new Vector2(pCoords.X / pCoords.Z, pCoords.Y / pCoords.Z); screenPos.X = 0.5f * width * (screenPos.X + 1f); screenPos.Y = 0.5f * height * (1f - screenPos.Y); return pCoords.Z > 0; } /// /// 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(Vector2 screenPos, out Vector3 worldPos, float rayDistance = 100000.0f) { // Get base object with matrices var matrixSingleton = this.getMatrixSingleton(); // Read current ViewProjectionMatrix plus game window size var viewProjectionMatrix = new 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 screenPos3D = new Vector3 { X = screenPos.X / width * 2.0f - 1.0f, Y = -(screenPos.Y / height * 2.0f - 1.0f), Z = 0 }; Vector3.TransformCoordinate(ref screenPos3D, ref viewProjectionMatrix, out var camPos); screenPos3D.Z = 1; 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 Vector3 { X = worldPosArray[0], Y = worldPosArray[1], Z = worldPosArray[2] }; } return isSuccess; } private IntPtr ToggleUiHideDetour(IntPtr thisPtr, byte unknownByte) { GameUiHidden = !GameUiHidden; try { OnUiHideToggled?.Invoke(this, GameUiHidden); } catch (Exception ex) { Log.Error(ex, "Error on OnUiHideToggled event dispatch"); } Log.Debug("UiHide toggled: {0}", GameUiHidden); return this.toggleUiHideHook.Original(thisPtr, unknownByte); } /// /// 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); } public Addon.Addon GetAddonByName(string name, int index) { var addonMem = GetUiObjectByName(name, index); if (addonMem == IntPtr.Zero) return null; var addonStruct = Marshal.PtrToStructure(addonMem); return new Addon.Addon(addonMem, addonStruct); } public void SetBgm(ushort bgmKey) => this.setGlobalBgmHook.Original(bgmKey, 0, 0, 0, 0, 0); public void Enable() { Chat.Enable(); this.setGlobalBgmHook.Enable(); this.handleItemHoverHook.Enable(); this.handleItemOutHook.Enable(); this.toggleUiHideHook.Enable(); this.handleActionHoverHook.Enable(); this.handleActionOutHook.Enable(); } public void Dispose() { Chat.Dispose(); this.setGlobalBgmHook.Dispose(); this.handleItemHoverHook.Dispose(); this.handleItemOutHook.Dispose(); this.toggleUiHideHook.Dispose(); this.handleActionHoverHook.Dispose(); this.handleActionOutHook.Dispose(); } } }