using System.Linq; using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.Config; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.Textures; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; using HellionChat.Code; using HellionChat.Resources; using HellionChat.Themes; using HellionChat.Ui.Components; using HellionChat.Ui.Windows; using HellionChat.Util; using Lumina.Excel.Sheets; using Microsoft.Extensions.Logging; using Action = System.Action; using ChatTwoPartyFinderPayload = HellionChat.Util.PartyFinderPayload; using DalamudPartyFinderPayload = Dalamud.Game.Text.SeStringHandling.Payloads.PartyFinderPayload; namespace HellionChat; internal sealed class PayloadHandler { private const string PopupId = "hellionchat-context-popup"; private const uint PopupSfx = 1; private readonly ThemeRegistry _themes; private readonly IpcManager _ipc; private readonly GameFunctions.GameFunctions _functions; private readonly InputBar _inputBar; private readonly MainWindow _mainWindow; private readonly ChunkRenderer _chunkRenderer; private readonly ILogger _logger; public bool HandleTooltips; public uint HoveredItem; public uint HoverCounter; public uint LastHoverCounter; private (Chunk, Payload?)? _popup; private object? _popupOwner; public PayloadHandler( ThemeRegistry themes, IpcManager ipc, GameFunctions.GameFunctions functions, InputBar inputBar, MainWindow mainWindow, ChunkRenderer chunkRenderer, ILogger logger ) { _themes = themes; _ipc = ipc; _functions = functions; _inputBar = inputBar; _mainWindow = mainWindow; _chunkRenderer = chunkRenderer; _logger = logger; } // Every chat surface shares this handler, so each one claims it before it // renders. Without that the popup cannot tell whose click it belongs to. internal object? ActiveSurface { get; set; } internal void Draw() { DrawPopups(); if (HandleTooltips && ++HoverCounter - LastHoverCounter > 1) { GameFunctions.GameFunctions.CloseItemTooltip(); HoveredItem = 0; HoverCounter = LastHoverCounter = 0; HandleTooltips = false; } } private void DrawPopups() { // A foreign surface finds no open popup here and would otherwise read // that as "closed" and drop the state before the owner gets to draw. if (_popup == null || !PopupOwnership.Owns(_popupOwner, ActiveSurface)) return; var (chunk, payload) = _popup.Value; using var popup = ImRaii.Popup(PopupId); if (!popup.Success) { _popup = null; _popupOwner = null; return; } using var id = ImRaii.PushId(PopupId); var drawn = false; switch (payload) { case PlayerPayload player: DrawPlayerPopup(chunk, player); drawn = true; break; case ItemPayload item: DrawItemPopup(item); drawn = true; break; case UriPayload uri: DrawUriPopup(uri); drawn = true; break; case StatusPayload status: DrawStatusPopup(status); drawn = true; break; } ContextFooter(drawn, chunk); Integrations(chunk, payload); } private void Integrations(Chunk chunk, Payload? payload) { var registered = _ipc.Registered; if (registered.Count == 0) return; ImGui.Separator(); var contentId = chunk.Message?.ContentId ?? 0; var sender = chunk.Message?.Sender.Select(c => c.Link).FirstOrDefault(p => p is PlayerPayload) as PlayerPayload; using var menu = ImRaii.Menu(Language.Context_Integrations); if (!menu.Success) return; var cursor = ImGui.GetCursorPos(); foreach (var integrationId in registered) { try { _ipc.Invoke( integrationId, sender, contentId, payload, chunk.Message?.SenderSource, chunk.Message?.ContentSource ); } catch (Exception ex) { _logger.LogError(ex, "Error executing integration"); } } if (cursor == ImGui.GetCursorPos()) { using var pushedColor = ImRaii.PushColor( ImGuiCol.Text, ImGui.GetStyle().Colors[(int)ImGuiCol.TextDisabled] ); ImGui.Text("No integrations available"); } } private void ContextFooter(bool didCustomContext, Chunk chunk) { ImRaii.MenuDisposable menu = default; if (didCustomContext) { ImGui.Separator(); // Only place these menu items in a submenu if we've already drawn // custom context menu items based on the payload. // // It makes it much more convenient in the majority of cases to // copy the message content without having to open a submenu. menu = ImRaii.Menu(Plugin.PluginName); if (!menu.Success) return; } // The flag is persisted, so every toggle has to write it. Without the // write it only sticks when some unrelated save happens to run after. if (ImGui.Checkbox(Language.Context_ScreenshotMode, ref Plugin.Config.ScreenshotMode)) Plugin.Instance.SaveConfig(); if (ImGui.Selectable(Language.Context_HideChat)) Plugin.Config.HideChat = true; if (chunk.Message is { } message) { if (ImGui.Selectable(Language.Context_Copy)) { ImGui.SetClipboardText(StringifyMessage(message, true)); WrapperUtil.AddNotification(Language.Context_CopySuccess, NotificationType.Info); } // Only show a separate "Copy content" option if the message has // Sender chunks, so it doesn't show for system messages. if (message.Sender.Count > 0 && ImGui.Selectable(Language.Context_CopyContent)) { ImGui.SetClipboardText(StringifyMessage(message)); WrapperUtil.AddNotification( Language.Context_CopyContentSuccess, NotificationType.Info ); } using var pushedColor = ImRaii.PushColor( ImGuiCol.Text, ImGui.GetStyle().Colors[(int)ImGuiCol.TextDisabled] ); ImGui.TextUnformatted(message.Code.Type.Name()); } menu.Dispose(); } private static string StringifyMessage(Message? message, bool withSender = false) { if (message == null) return string.Empty; var chunks = withSender ? message.Sender.Concat(message.Content) : message.Content; return chunks .Where(chunk => chunk is TextChunk) .Cast() .Select(text => text.Content) .Aggregate(string.Empty, string.Concat); } private void DrawPlayerPopup(Chunk chunk, PlayerPayload player) { // Possible that GMs return a null payload // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract if (player == null) return; var world = player.World; if (chunk.Message?.Code.Type == ChatType.FreeCompanyLoginLogout) if (Plugin.PlayerState.HomeWorld.IsValid) world = Plugin.PlayerState.HomeWorld; var name = new List { new TextChunk(ChunkSource.None, null, player.PlayerName) }; if (world.Value.IsPublic) { name.AddRange([ new IconChunk(ChunkSource.None, null, BitmapFontIcon.CrossWorld), new TextChunk(ChunkSource.None, null, world.Value.Name.ExtractText()), ]); } _chunkRenderer.DrawChunks(name, false); ImGui.Separator(); var validContentId = chunk.Message?.ContentId is not (null or 0); if (ImGui.Selectable(Language.Context_SendTell)) { // Eureka, Bozja and Occult need special handling as tells work different if (!Sheets.IsInForay()) { // Single SetPendingMessage call; v1.5.6 used incremental Chat += writes. // Shares the /tell builder with the native detours. IsPublic (not // IsNullOrEmpty) is resolved HERE — a private/null world must NOT leak @World. _inputBar.SetPendingMessage( GameFunctions.Chat.BuildTellCommand( player.PlayerName, world.Value.IsPublic ? world.Value.Name.ToString() : null ) ); } else if (validContentId) { _functions.Chat.SetEurekaTellChannel( player.PlayerName, world.Value.Name.ToString(), (ushort)world.RowId, 0, chunk.Message!.ContentId, 0, false ); } _inputBar.Activate = true; } if (world.Value.IsPublic) { var party = Plugin.PartyList; var leader = party[(int)party.PartyLeaderIndex]?.ContentId; var isLeader = party.Length == 0 || Plugin.PlayerState.ContentId == leader; var member = party.FirstOrDefault(member => member.Name.TextValue == player.PlayerName && member.World.RowId == world.RowId ); var isInParty = member != null; var inInstance = GameFunctions.GameFunctions.IsInInstance(); var inPartyInstance = Sheets .TerritorySheet.GetRow(Plugin.ClientState.TerritoryType) .TerritoryIntendedUse.RowId is (41 or 47 or 48 or 52 or 53 or 61); if (isLeader) { if (!isInParty) { if (inInstance && inPartyInstance) { if (validContentId && ImGui.Selectable(Language.Context_InviteToParty)) GameFunctions.Party.InviteInInstance(chunk.Message!.ContentId); } else if (!inInstance) { using var menu = ImRaii.Menu(Language.Context_InviteToParty); if (menu.Success) { if (ImGui.Selectable(Language.Context_InviteToParty_SameWorld)) GameFunctions.Party.InviteSameWorld( player.PlayerName, (ushort)world.RowId, chunk.Message?.ContentId ?? 0 ); if ( validContentId && ImGui.Selectable(Language.Context_InviteToParty_DifferentWorld) ) GameFunctions.Party.InviteOtherWorld( chunk.Message!.ContentId, (ushort)world.RowId ); } } } if (isInParty && member != null && (!inInstance || (inInstance && inPartyInstance))) { if (ImGui.Selectable(Language.Context_Promote)) GameFunctions.Party.Promote(player.PlayerName, member.ContentId); if (ImGui.Selectable(Language.Context_KickFromParty)) GameFunctions.Party.Kick(player.PlayerName, member.ContentId); } } var isFriend = GameFunctions .GameFunctions.GetFriends() .Any(friend => friend.NameString == player.PlayerName && friend.HomeWorld == world.RowId ); if (!isFriend && ImGui.Selectable(Language.Context_SendFriendRequest)) _functions.SendFriendRequest(player.PlayerName, (ushort)world.RowId); using (var menuBlockFunctions = ImRaii.Menu(Language.Context_BlockFunctions)) { if (menuBlockFunctions.Success) { if (ImGui.Selectable(Language.Context_AddToBlacklist)) _functions.AddToBlacklist(player.PlayerName, (ushort)world.RowId); if (chunk.Message != null) { var message = chunk.Message; if ( message.AccountId != 0 && ImGui.Selectable(Language.Context_AddToMuteList) ) _functions.AddToMuteList( message.AccountId, message.ContentId, player.PlayerName, (short)world.RowId ); if (ImGui.Selectable(Language.Context_AddToTermsFilter)) _functions.AddToTermsList(message.ContentSource); } } } if ( GameFunctions.GameFunctions.IsMentor() && ImGui.Selectable(Language.Context_InviteToNoviceNetwork) ) GameFunctions.Context.InviteToNoviceNetwork(player.PlayerName, (ushort)world.RowId); } var inputChannel = chunk.Message?.Code.Type.ToInputChannel(); if (inputChannel != null && ImGui.Selectable(Language.Context_ReplyInSelectedChatMode)) { // Route the channel switch through MainWindow's active tab _mainWindow.ActiveTab?.CurrentChannel?.SetChannel(inputChannel.Value); _inputBar.Activate = true; } if (ImGui.Selectable(Language.Context_Target) && FindCharacterForPayload(player) is { } obj) Plugin.TargetManager.Target = obj; if (validContentId && ImGui.Selectable(Language.Context_AdventurerPlate)) if (!GameFunctions.GameFunctions.TryOpenAdventurerPlate(chunk.Message!.ContentId)) WrapperUtil.AddNotification( Language.Context_AdventurerPlateError, NotificationType.Warning ); } // Returns the first matching IPlayerCharacter in ObjectTable, null if out of render range. private IPlayerCharacter? FindCharacterForPayload(PlayerPayload payload) { foreach (var obj in Plugin.ObjectTable) { if (obj is not IPlayerCharacter character) continue; if (character.Name.TextValue != payload.PlayerName) continue; if (payload.World.Value.IsPublic && character.HomeWorld.RowId != payload.World.RowId) continue; return character; } return null; } private void DrawItemPopup(ItemPayload payload) { if (payload.Kind == ItemKind.EventItem) { DrawEventItemPopup(payload); return; } if (!Sheets.ItemSheet.TryGetRow(payload.ItemId, out var itemRow)) return; var hq = payload.Kind == ItemKind.Hq; if ( Plugin .TextureProvider.GetFromGameIcon(new GameIconLookup(itemRow.Icon, hq)) .GetWrapOrDefault() is { } icon ) InlineIcon(icon); var name = itemRow.Name.ToDalamudString(); if (hq) // hq symbol name.Payloads.Add(new TextPayload(" ")); else if (payload.Kind == ItemKind.Collectible) name.Payloads.Add(new TextPayload(" ")); _chunkRenderer.DrawChunks(ChunkUtil.ToChunks(name, ChunkSource.None, null).ToList(), false); ImGui.Separator(); var realItemId = payload.RawItemId; if (itemRow.EquipSlotCategory.RowId != 0) { if (ImGui.Selectable(Language.Context_TryOn)) GameFunctions.Context.TryOn(realItemId, 0); if (ImGui.Selectable(Language.Context_ItemComparison)) GameFunctions.Context.OpenItemComparison(realItemId); } if (itemRow.ItemSearchCategory.Value.Category == 3) if (ImGui.Selectable(Language.Context_SearchRecipes)) GameFunctions.Context.SearchForRecipesUsingItem(payload.ItemId); if (ImGui.Selectable(Language.Context_SearchForItem)) GameFunctions.Context.SearchForItem(realItemId); if (ImGui.Selectable(Language.Context_Link)) GameFunctions.Context.LinkItem(realItemId); if (ImGui.Selectable(Language.Context_CopyItemName)) ImGui.SetClipboardText(name.TextValue); } private void DrawEventItemPopup(ItemPayload payload) { if (payload.Kind != ItemKind.EventItem) return; if (!Sheets.EventItemSheet.HasRow(payload.ItemId)) return; var item = Sheets.EventItemSheet.GetRow(payload.ItemId); if ( Plugin .TextureProvider.GetFromGameIcon(new GameIconLookup(item.Icon)) .GetWrapOrDefault() is { } icon ) InlineIcon(icon); _chunkRenderer.DrawChunks( ChunkUtil.ToChunks(item.Name.ToDalamudString(), ChunkSource.None, null).ToList(), false ); ImGui.Separator(); var realItemId = payload.RawItemId; if (ImGui.Selectable(Language.Context_Link)) GameFunctions.Context.LinkItem(realItemId); if (ImGui.Selectable(Language.Context_CopyItemName)) ImGui.SetClipboardText(item.Name.ToString()); } private void DrawStatusPopup(StatusPayload status) { if ( Plugin .TextureProvider.GetFromGameIcon(new GameIconLookup(status.Status.Value.Icon)) .GetWrapOrDefault() is { } icon ) InlineIcon(icon); var builder = new SeStringBuilder(); var nameValue = status.Status.Value.Name.ToString(); switch (status.Status.Value.StatusCategory) { case 1: builder.AddUiForeground($"{SeIconChar.Buff.ToIconString()}{nameValue}", 517); break; case 2: builder.AddUiForeground($"{SeIconChar.Debuff.ToIconString()}{nameValue}", 518); break; default: builder.AddUiForeground(nameValue, 1); break; } _chunkRenderer.DrawChunks( ChunkUtil.ToChunks(builder.BuiltString, ChunkSource.None, null).ToList(), false ); ImGui.Separator(); if (ImGui.Selectable(Language.Context_Link)) { GameFunctions.Context.LinkStatus(status.Status.RowId); _inputBar.AppendPending(" "); } } private void DrawUriPopup(UriPayload uri) { ImGui.TextUnformatted(string.Format(Language.Context_URLDomain, uri.Uri.Authority)); ImGuiUtil.WarningText(Language.Context_URLWarning, false); ImGui.Separator(); if (ImGui.Selectable(Language.Context_OpenInBrowser)) WrapperUtil.TryOpenUri(uri.Uri); if (ImGui.Selectable(Language.Context_CopyLink)) { ImGui.SetClipboardText(uri.Uri.ToString()); WrapperUtil.AddNotification( Language.Context_CopyLinkNotification, NotificationType.Info ); } } public unsafe void MoveTooltip(AddonEvent type, AddonArgs args) { // Defensive guard added in v1.7.1 — AddonLifecycle should never pass null, but be safe. if (args == null) { _logger.LogWarning("MoveTooltip called with null AddonArgs — unexpected, skipping"); return; } // Only move if the user has the "Next to Cursor" option selected if ( !Plugin.GameConfig.TryGet(UiControlOption.DetailTrackingType, out uint selected) || selected != 0 ) return; if (_mainWindow.LastViewport != ImGuiHelpers.MainViewport.Handle) return; var atk = args.Addon; if (atk.IsNull) return; var atkBase = (AtkUnitBase*)atk.Address; if (atkBase->WindowNode == null) return; if (!atkBase->IsVisible) return; var component = atkBase->WindowNode->AtkResNode; var atkPos = new Vector2(component.ScreenX, component.ScreenY); var atkSize = new Vector2( component.GetWidth() * component.ScaleX, component.GetHeight() * component.GetScaleY() ); var chatRect = new MathUtil.Rectangle( _mainWindow.LastWindowPos, _mainWindow.LastWindowSize ); var addonRect = new MathUtil.Rectangle(atkPos, atkSize); if (!chatRect.HasOverlap(addonRect)) return; var viewportSize = ImGuiHelpers.MainViewport.Size; var isLeft = chatRect.SizeX < viewportSize.X / 2; var isTop = chatRect.SizeY < viewportSize.Y / 2; var mousePos = ImGui.GetMousePos(); // addon spawned left of mouse cursor if (addonRect.X < mousePos.X) { if (isLeft) addonRect.X = (short)mousePos.X + 5; } else { if (!isLeft) addonRect.X = Math.Max(0, (short)mousePos.X - 5 - addonRect.Width); } if (!chatRect.HasOverlap(addonRect)) { atkBase->SetPosition((short)addonRect.X, (short)addonRect.Y); return; } // addon spawned above mouse cursor if (addonRect.Y < mousePos.Y) { if (isTop) addonRect.Y = (short)mousePos.Y + 5; } else { if (!isTop) addonRect.Y = Math.Max(0, (short)mousePos.Y - 5 - addonRect.Height); // prevent it going below 0 } if (!chatRect.HasOverlap(addonRect)) { atkBase->SetPosition((short)addonRect.X, (short)addonRect.Y); return; } // Spawning right/bottom of mouse cursor didn't solve the overlap, so we spawn it next to the chat var x = isLeft ? chatRect.SizeX : _mainWindow.LastWindowPos.X - atkSize.X; var y = Math.Clamp(chatRect.SizeY - atkSize.Y, 0, float.MaxValue); y -= isTop ? 0 : Plugin.Config.TooltipOffset; // offset to prevent cut-off on the bottom atkBase->SetPosition((short)x, (short)y); } private const float MaxInlineIconSize = 32f; private static void InlineIcon(IDalamudTextureWrap icon) { if (icon.Size.X <= 0 || icon.Size.Y <= 0) return; var width = (float)icon.Size.X; var height = (float)icon.Size.Y; var scale = Math.Min(1f, Math.Min(MaxInlineIconSize / width, MaxInlineIconSize / height)); var size = ImGuiHelpers.ScaledVector2(width * scale, height * scale); var cursor = ImGui.GetCursorPos(); ImGui.Image(icon.Handle, size); ImGui.SameLine(); ImGui.SetCursorPos( cursor + new Vector2(size.X + 4, size.Y - ImGui.GetTextLineHeightWithSpacing()) ); } internal void Hover(Payload payload) { var hoverSize = 350f * ImGuiHelpers.GlobalScale; switch (payload) { case StatusPayload status: DoHover(() => HoverStatus(status), hoverSize); break; case ItemPayload item: // Native tooltip path: set state for MoveTooltip to reposition the game addon next frame. if (Plugin.Config.NativeItemTooltips) { if (!HandleTooltips || HoveredItem != item.RawItemId) { HandleTooltips = true; HoveredItem = item.RawItemId; HoverCounter = LastHoverCounter = 0; GameFunctions.GameFunctions.OpenItemTooltip(item.RawItemId, item.Kind); } else { LastHoverCounter = HoverCounter; } return; } DoHover(() => HoverItem(item), hoverSize); break; case UriPayload uri: DoHover(() => HoverUri(uri), hoverSize); break; } } private void DoHover(Action drawAction, float tooltipWidth) { ImGui.SetNextWindowSize(new Vector2(tooltipWidth, -1f)); using (ImRaii.Tooltip()) using (ImRaii.TextWrapPos(0.0f)) using ( // Use the active theme text colour instead of the former LogWindow.DefaultText static. ImRaii.PushColor( ImGuiCol.Text, ColourUtil.RgbaToVector4(_themes.Active.Colors.TextPrimary) ) ) drawAction(); } private void HoverStatus(StatusPayload status) { if ( Plugin.TextureProvider.GetFromGameIcon(status.Status.Value.Icon).GetWrapOrDefault() is { } icon ) InlineIcon(icon); var builder = new SeStringBuilder(); var nameValue = status.Status.Value.Name.ToString(); switch (status.Status.Value.StatusCategory) { case 1: builder.AddUiForeground($"{SeIconChar.Buff.ToIconString()}{nameValue}", 517); break; case 2: builder.AddUiForeground($"{SeIconChar.Debuff.ToIconString()}{nameValue}", 518); break; default: builder.AddUiForeground(nameValue, 1); break; } var name = ChunkUtil.ToChunks(builder.BuiltString, ChunkSource.None, null); _chunkRenderer.DrawChunks(name.ToList()); ImGui.Separator(); var desc = ChunkUtil.ToChunks( status.Status.Value.Description.ToDalamudString(), ChunkSource.None, null ); _chunkRenderer.DrawChunks(desc.ToList()); } private void HoverItem(ItemPayload item) { if (item.Kind == ItemKind.EventItem) { HoverEventItem(item); return; } if (!Sheets.ItemSheet.TryGetRow(item.ItemId, out var resolvedItem)) return; if ( Plugin .TextureProvider.GetFromGameIcon( new GameIconLookup(resolvedItem.Icon, item.Kind == ItemKind.Hq) ) .GetWrapOrDefault() is { } icon ) InlineIcon(icon); var name = ChunkUtil.ToChunks(resolvedItem.Name.ToDalamudString(), ChunkSource.None, null); _chunkRenderer.DrawChunks(name.ToList()); ImGui.Separator(); var desc = ChunkUtil.ToChunks( resolvedItem.Description.ToDalamudString(), ChunkSource.None, null ); _chunkRenderer.DrawChunks(desc.ToList()); } private void HoverEventItem(ItemPayload item) { if (!Sheets.EventItemSheet.TryGetRow(item.RawItemId, out var itemRow)) return; if ( Plugin .TextureProvider.GetFromGameIcon(new GameIconLookup(itemRow.Icon)) .GetWrapOrDefault() is { } icon ) InlineIcon(icon); var name = ChunkUtil.ToChunks(itemRow.Name.ToDalamudString(), ChunkSource.None, null); _chunkRenderer.DrawChunks(name.ToList()); ImGui.Separator(); if (!Sheets.EventItemHelpSheet.TryGetRow(item.RawItemId, out var itemHelpRow)) return; _chunkRenderer.DrawChunks( ChunkUtil .ToChunks(itemHelpRow.Description.ToDalamudString(), ChunkSource.None, null) .ToList() ); } private void HoverUri(UriPayload uri) { ImGui.TextUnformatted(string.Format(Language.Context_URLDomain, uri.Uri.Authority)); ImGuiUtil.WarningText(Language.Context_URLWarning); } internal unsafe void Click(Chunk chunk, Payload? payload, ImGuiMouseButton button) { if (Plugin.Config.PlaySounds) UIGlobals.PlaySoundEffect(PopupSfx); switch (button) { case ImGuiMouseButton.Left: LeftClickPayload(chunk, payload); break; case ImGuiMouseButton.Right: RightClickPayload(chunk, payload); break; } } private void LeftClickPayload(Chunk chunk, Payload? payload) { switch (payload) { case MapLinkPayload map: Plugin.GameGui.OpenMapWithMapLink(map); break; case QuestPayload quest: GameFunctions.GameFunctions.OpenQuestLog(quest.Quest); break; case DalamudLinkPayload link: ClickLinkPayload(chunk, payload, link); break; case DalamudPartyFinderPayload pf: if ( pf.LinkType == DalamudPartyFinderPayload.PartyFinderLinkType.PartyFinderNotification ) GameFunctions.GameFunctions.OpenPartyFinder(); else GameFunctions.GameFunctions.OpenPartyFinder(pf.ListingId); break; case ChatTwoPartyFinderPayload pf: GameFunctions.GameFunctions.OpenPartyFinder(pf.Id); break; case AchievementPayload achievement: GameFunctions.GameFunctions.OpenAchievement(achievement.Id); break; case RawPayload raw: if (Equals(raw, ChunkUtil.PeriodicRecruitmentLink)) GameFunctions.GameFunctions.OpenPartyFinder(); break; case UriPayload uri: WrapperUtil.TryOpenUri(uri.Uri); break; default: RightClickPayload(chunk, payload); break; } } private void ClickLinkPayload(Chunk chunk, Payload payload, DalamudLinkPayload link) { if (chunk.GetSeString() is not { } source) return; var start = source.Payloads.IndexOf(payload); var end = source.Payloads.IndexOf(RawPayload.LinkTerminator, start == -1 ? 0 : start); if (start == -1 || end == -1) return; var payloads = source.Payloads.Skip(start).Take(end - start + 1).ToList(); if ( !Plugin.ChatGui.RegisteredLinkHandlers.TryGetValue( (link.Plugin, link.CommandId), out var value ) ) { _logger.LogWarning("Could not find DalamudLinkHandlers"); return; } try { // Running XivCommon SendChat instantly, without RunOnTick, leads to a game freeze, for whatever reason Plugin.Framework.RunOnTick(() => value.Invoke(link.CommandId, new SeString(payloads))); } catch (Exception ex) { _logger.LogError(ex, "Error executing DalamudLinkPayload handler"); } } private void RightClickPayload(Chunk chunk, Payload? payload) { _popup = (chunk, payload); _popupOwner = ActiveSurface; ImGui.OpenPopup(PopupId); } }