using System.Globalization; using System.Runtime.InteropServices; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Hooking; using Dalamud.Memory; using Dalamud.Utility; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Enums; using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Info; using FFXIVClientStructs.FFXIV.Component.GUI; using Lumina.Excel; using Lumina.Excel.Sheets; using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.AtkValueType; namespace HellionChat.GameFunctions; internal unsafe class GameFunctions : IDisposable { internal const string NewGamePlusAddonName = "QuestRedo"; #region Hooks [Signature( "E8 ?? ?? ?? ?? 48 85 C0 0F 84 ?? ?? ?? ?? 48 8B D0 49 8D 4F", DetourName = nameof(ResolveTextCommandPlaceholderDetour) )] private Hook? ResolveTextCommandPlaceholderHook = null!; private delegate nint ResolveTextCommandPlaceholderDelegate( nint a1, byte* placeholderText, byte a3, byte a4 ); #endregion private Plugin Plugin { get; } internal KeybindManager KeybindManager { get; } internal Chat Chat { get; } internal GameFunctions(Plugin plugin) { Plugin = plugin; KeybindManager = new KeybindManager(plugin); Chat = new Chat(Plugin); Plugin.GameInteropProvider.InitializeFromAttributes(this); ResolveTextCommandPlaceholderHook?.Enable(); } public void Dispose() { Chat.Dispose(); KeybindManager.Dispose(); ResolveTextCommandPlaceholderHook?.Dispose(); Marshal.FreeHGlobal(PlaceholderNamePtr); } internal void SendFriendRequest(string name, ushort world) => ListCommand(name, world, "friendlist"); internal void AddToBlacklist(string name, ushort world) => ListCommand(name, world, "blist"); internal void AddToMuteList(ulong accountId, ulong contentId, string name, short worldId) => AgentMutelist.Instance()->Add(accountId, contentId, name, worldId); internal void AddToTermsList(SeString content) => AgentTermFilter.Instance()->OpenNewFilterWindow(content.EncodeWithNullTerminator()); private void ListCommand(string name, ushort world, string commandName) { var worldRow = Sheets.WorldSheet.GetRow(world); ReplacementName = $"{name}@{worldRow.Name.ToString()}"; ChatBox.SendMessage($"/{commandName} add {Placeholder}"); } private static T* GetAddon(string name) where T : unmanaged { var addon = RaptureAtkModule.Instance()->RaptureAtkUnitManager.GetAddonByName(name); return addon != null && addon->IsReady ? (T*)addon : null; } internal static void SetAddonInteractable(string name, bool interactable) { var addon = GetAddon(name); if (addon == null) return; addon->IsVisible = interactable; } internal static void SetChatInteractable(bool interactable) { for (var i = 0; i < 4; i++) SetAddonInteractable($"ChatLogPanel_{i}", interactable); SetAddonInteractable("ChatLog", interactable); } internal static bool IsAddonInteractable(string name) { var addon = GetAddon(name); return addon != null && addon->IsVisible; } internal static void OpenItemTooltip(uint id, ItemKind itemKind) { var atkStage = AtkStage.Instance(); var agent = AgentItemDetail.Instance(); var addon = GetAddon("ItemDetail"); if (agent == null || addon == null) return; agent->DetailKind = itemKind == ItemKind.EventItem ? DetailKind.KeyItem : DetailKind.Item; agent->TypeOrId = id; agent->Index = 0; agent->Flag1 &= 0xEF; agent->ItemId = id; // TODO: Revert when CS offset lands in a release build. *(byte*)((nint)agent + 0x21A) = 1; *(byte*)((nint)agent + 0x21E) = 0; agent->AddonId = addon->Id; atkStage->TooltipManager.TooltipType |= 2; addon->Show(false, 15); } internal static void CloseItemTooltip() { // Hide addon first to suppress the "addon close" sound. var addon = GetAddon("ItemDetail"); if (addon != null) addon->Hide(true, false, 0); var agent = AgentItemDetail.Instance(); if (agent != null) { var eventData = stackalloc AtkValue[1]; var atkValues = stackalloc AtkValue[1]; atkValues->Type = ValueType.Int; atkValues->Int = -1; agent->ReceiveEvent(eventData, atkValues, 1, 1); } } internal static void OpenPartyFinder() { // 6.05: 84433A (FF 97 ?? ?? ?? ?? 41 B4 01) var lfg = AgentLookingForGroup.Instance(); if (lfg->IsAgentActive()) { var addonId = lfg->GetAddonId(); var atkModule = RaptureAtkModule.Instance(); var atkModuleVtbl = (void**)atkModule->AtkModule.VirtualTable; var vf27 = (delegate* unmanaged) atkModuleVtbl[27]; vf27(atkModule, addonId, 1); } else { // 6.05: 8443DD if (*(uint*)((nint)lfg + 0x2C20) > 0) lfg->Hide(); else lfg->Show(); } } internal static bool IsMentor() => PlayerState.Instance()->IsMentor(); internal static InfoProxyCommonList.CharacterData[] GetFriends() => InfoProxyFriendList.Instance()->CharDataSpan.ToArray(); internal static void OpenQuestLog(RowRef quest) { var splits = quest.Value.Id.ToString().Split("_"); if (splits.Length != 2) { Plugin.ChatGui.Print("QuestId is wrongly formatted"); return; } if ( !uint.TryParse( splits[1], NumberStyles.Any, CultureInfo.InvariantCulture, out var questId ) ) { Plugin.ChatGui.Print("Unable to parse quest id"); return; } AgentQuestJournal.Instance()->OpenForQuest(questId, 1); } internal static void OpenPartyFinder(uint id) => AgentLookingForGroup.Instance()->OpenListing(id); internal static void OpenAchievement(uint id) => AgentAchievement.Instance()->OpenById(id); internal static bool IsInInstance() => Plugin.Condition[ConditionFlag.BoundByDuty56]; internal static bool TryOpenAdventurerPlate(ulong playerId) { try { AgentCharaCard.Instance()->OpenCharaCard(playerId); return true; } catch (Exception e) { Plugin.Log.Warning(e, "Unable to open adventurer plate"); return false; } } internal static void ClickNoviceNetworkButton() { var agent = AgentChatLog.Instance(); var value = new AtkValue { Type = ValueType.Int, Int = 3 }; // case 3 var result = 0; var vf0 = *(delegate* unmanaged*) agent->VirtualTable; vf0(agent, &result, &value, 0, 0); } private const int PlaceholderBufferSize = 128; private readonly nint PlaceholderNamePtr = Marshal.AllocHGlobal(PlaceholderBufferSize); private readonly string Placeholder = $"<{Guid.NewGuid():N}>"; private string? ReplacementName; private nint ResolveTextCommandPlaceholderDetour( nint a1, byte* placeholderText, byte a3, byte a4 ) { // Hook field is nullable due to the Signature attribute, but will never // be null during normal execution; guard covers the teardown race only. if (ResolveTextCommandPlaceholderHook is null) return nint.Zero; var placeholder = MemoryHelper.ReadStringNullTerminated((nint)placeholderText); if (ReplacementName == null || placeholder != Placeholder) return ResolveTextCommandPlaceholderHook.Original(a1, placeholderText, a3, a4); // Guard against a malformed ReplacementName overflowing the 128-byte buffer. var byteCount = System.Text.Encoding.UTF8.GetByteCount(ReplacementName); if (byteCount >= PlaceholderBufferSize) { Plugin.Log.Warning( $"Replacement name too long for placeholder buffer ({byteCount} bytes >= {PlaceholderBufferSize}); falling back to original." ); ReplacementName = null; return ResolveTextCommandPlaceholderHook.Original(a1, placeholderText, a3, a4); } MemoryHelper.WriteString(PlaceholderNamePtr, ReplacementName); ReplacementName = null; return PlaceholderNamePtr; } }