using System.Numerics; using ChatTwo.Code; using ChatTwo.Resources; using ChatTwo.Ui; using ChatTwo.Util; 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.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.LayoutEngine; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; using ImGuiNET; using Lumina.Excel.Sheets; using Action = System.Action; using DalamudPartyFinderPayload = Dalamud.Game.Text.SeStringHandling.Payloads.PartyFinderPayload; using ChatTwoPartyFinderPayload = ChatTwo.Util.PartyFinderPayload; namespace ChatTwo; public sealed class PayloadHandler { private const string PopupId = "chat2-context-popup"; private ChatLogWindow LogWindow { get; } private (Chunk, Payload?)? Popup { get; set; } public bool HandleTooltips; public uint HoveredItem; public uint HoverCounter; public uint LastHoverCounter; private const uint PopupSfx = 1u; internal PayloadHandler(ChatLogWindow logWindow) { LogWindow = logWindow; } internal void Draw() { DrawPopups(); if (HandleTooltips && ++HoverCounter - LastHoverCounter > 1) { GameFunctions.GameFunctions.CloseItemTooltip(); HoveredItem = 0; HoverCounter = LastHoverCounter = 0; HandleTooltips = false; } } private void DrawPopups() { if (Popup == null) return; var (chunk, payload) = Popup.Value; using var popup = ImRaii.Popup(PopupId); if (!popup.Success) { Popup = 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; } ContextFooter(drawn, chunk); Integrations(chunk, payload); } private void Integrations(Chunk chunk, Payload? payload) { var registered = LogWindow.Plugin.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; if (ImGui.BeginMenu(Language.Context_Integrations)) { var cursor = ImGui.GetCursorPos(); foreach (var id in registered) { try { LogWindow.Plugin.Ipc.Invoke(id, sender, contentId, payload, chunk.Message?.SenderSource, chunk.Message?.ContentSource); } catch (Exception ex) { Plugin.Log.Error(ex, "Error executing integration"); } } if (cursor == ImGui.GetCursorPos()) { ImGui.PushStyleColor(ImGuiCol.Text, ImGui.GetStyle().Colors[(int) ImGuiCol.TextDisabled]); ImGui.Text("No integrations available"); ImGui.PopStyleColor(); } ImGui.EndMenu(); } } private void ContextFooter(bool didCustomContext, Chunk chunk) { 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. if (!ImGui.BeginMenu(Plugin.PluginName)) return; } // ScreenshotMode changed, so we inform the webinterface about the new message format if (ImGui.Checkbox(Language.Context_ScreenshotMode, ref LogWindow.ScreenshotMode)) LogWindow.Plugin.ServerCore.SendBulkMessageList(); if (ImGui.Selectable(Language.Context_HideChat)) LogWindow.UserHide(); 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); } var col = ImGui.GetStyle().Colors[(int) ImGuiCol.TextDisabled]; ImGui.PushStyleColor(ImGuiCol.Text, col); try { ImGui.TextUnformatted(message.Code.Type.Name()); } finally { ImGui.PopStyleColor(); } } if (didCustomContext) ImGui.EndMenu(); } 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.Concat); } internal void Click(Chunk chunk, Payload? payload, ImGuiMouseButton button) { if (Plugin.Config.PlaySounds) UIModule.PlaySound(PopupSfx); switch (button) { case ImGuiMouseButton.Left: LeftClickPayload(chunk, payload); break; case ImGuiMouseButton.Right: RightClickPayload(chunk, payload); break; } } internal void Hover(Payload payload) { var hoverSize = 350f * ImGuiHelpers.GlobalScale; switch (payload) { case StatusPayload status: DoHover(() => HoverStatus(status), hoverSize); break; case ItemPayload item: 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 inside, float width) { ImGui.SetNextWindowSize(new Vector2(width, -1f)); using var tooltip = ImRaii.Tooltip(); using var color = ImRaii.PushColor(ImGuiCol.Text, LogWindow.DefaultText); using var wrap = ImGuiUtil.TextWrapPos(); inside(); } public unsafe void MoveTooltip(AddonEvent type, AddonArgs args) { if (!HandleTooltips) return; // Only move if user has "Next to Cursor" option selected if (!Plugin.GameConfig.TryGet(UiControlOption.DetailTrackingType, out uint selected) || selected != 0) return; if (LogWindow.LastViewport != ImGuiHelpers.MainViewport.NativePtr) return; var atk = (AtkUnitBase*) args.Addon; if (atk->WindowNode == null) return; var component = atk->WindowNode->AtkResNode; var atkSize = (X: component.GetWidth() * component.ScaleX, Y: component.GetHeight() * component.GetScaleY()); var viewportSize = ImGuiHelpers.MainViewport.Size; var window = LogWindow.LastWindowPos + LogWindow.LastWindowSize; var isLeft = window.X < viewportSize.X / 2; var isTop = window.Y < viewportSize.Y / 2; var x = isLeft ? window.X : LogWindow.LastWindowPos.X - atkSize.X; var y = Math.Clamp(window.Y - atkSize.Y, 0, float.MaxValue); y -= isTop ? 0 : Plugin.Config.TooltipOffset; // offset to prevent cut-off on the bottom atk->SetPosition((short) x, (short) y); } private static void InlineIcon(IDalamudTextureWrap icon) { var lineHeight = ImGui.CalcTextSize("A").Y; var cursor = ImGui.GetCursorPos(); var size = new Vector2(icon.Width, icon.Height) * ImGuiHelpers.GlobalScale; ImGui.Image(icon.ImGuiHandle, size); ImGui.SameLine(); ImGui.SetCursorPos(cursor + new Vector2(size.X + 4, size.Y / 2 - lineHeight / 2)); } private static unsafe SeString ResolveRsv(SeString input) { var txt = input.TextValue; if (txt.StartsWith("_rsv_")) input = SeString.Parse(LayoutWorld.Instance()->ResolveRsvString(txt)); return input; } private void HoverStatus(StatusPayload status) { if (Plugin.TextureProvider.GetFromGameIcon(status.Status.Value.Icon).GetWrapOrDefault() is { } icon) InlineIcon(icon); var nameString = ResolveRsv(status.Status.Value.Name.ToDalamudString()); var name = ChunkUtil.ToChunks(nameString, ChunkSource.None, null); LogWindow.DrawChunks(name.ToList()); ImGui.Separator(); var descString = ResolveRsv(status.Status.Value.Description.ToDalamudString()); var desc = ChunkUtil.ToChunks(descString, ChunkSource.None, null); LogWindow.DrawChunks(desc.ToList()); } private void HoverItem(ItemPayload item) { if (item.Kind == ItemPayload.ItemKind.EventItem) { HoverEventItem(item); return; } item.Item.TryGetValue(out Item resolvedItem); if (Plugin.TextureProvider.GetFromGameIcon(new GameIconLookup(resolvedItem.Icon, item.IsHQ)).GetWrapOrDefault() is { } icon) InlineIcon(icon); var name = ChunkUtil.ToChunks(resolvedItem.Name.ToDalamudString(), ChunkSource.None, null); LogWindow.DrawChunks(name.ToList()); ImGui.Separator(); var desc = ChunkUtil.ToChunks(resolvedItem.Description.ToDalamudString(), ChunkSource.None, null); LogWindow.DrawChunks(desc.ToList()); } private void HoverEventItem(ItemPayload payload) { if (!Sheets.EventItemSheet.HasRow(payload.RawItemId)) return; var item = Sheets.EventItemSheet.GetRow(payload.RawItemId); if (Plugin.TextureProvider.GetFromGameIcon(new GameIconLookup(item.Icon)).GetWrapOrDefault() is { } icon) InlineIcon(icon); var name = ChunkUtil.ToChunks(item.Name.ToDalamudString(), ChunkSource.None, null); LogWindow.DrawChunks(name.ToList()); ImGui.Separator(); if (!Sheets.EventItemHelpSheet.HasRow(payload.RawItemId)) return; var help = Sheets.EventItemHelpSheet.GetRow(payload.RawItemId); LogWindow.DrawChunks(ChunkUtil.ToChunks(help.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); } 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)) { Plugin.Log.Warning("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) { Plugin.Log.Error(ex, "Error executing DalamudLinkPayload handler"); } } private void RightClickPayload(Chunk chunk, Payload? payload) { Popup = (chunk, payload); ImGui.OpenPopup(PopupId); } private void DrawItemPopup(ItemPayload payload) { if (payload.Kind == ItemPayload.ItemKind.EventItem) { DrawEventItemPopup(payload); return; } if (!Sheets.ItemSheet.HasRow(payload.ItemId)) return; var item = Sheets.ItemSheet.GetRow(payload.ItemId); var hq = payload.Kind == ItemPayload.ItemKind.Hq; if (Plugin.TextureProvider.GetFromGameIcon(new GameIconLookup(item.Icon, hq)).GetWrapOrDefault() is { } icon) InlineIcon(icon); var name = item.Name.ToDalamudString(); // hq symbol if (hq) name.Payloads.Add(new TextPayload(" ")); else if (payload.Kind == ItemPayload.ItemKind.Collectible) name.Payloads.Add(new TextPayload(" ")); LogWindow.DrawChunks(ChunkUtil.ToChunks(name, ChunkSource.None, null).ToList(), false); ImGui.Separator(); var realItemId = payload.RawItemId; if (item.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 (item.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 != ItemPayload.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); var name = item.Name.ToDalamudString(); LogWindow.DrawChunks(ChunkUtil.ToChunks(name, 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(name.TextValue); } private void DrawPlayerPopup(Chunk chunk, PlayerPayload player) { // Possible that GMs return a null payload if (player == null) return; var world = player.World; if (chunk.Message?.Code.Type == ChatType.FreeCompanyLoginLogout) if (Plugin.ClientState.LocalPlayer?.HomeWorld.IsValid == true) world = Plugin.ClientState.LocalPlayer.HomeWorld; var name = new List { new TextChunk(ChunkSource.None, null, player.PlayerName) }; if (world.Value.IsPublic) { name.AddRange(new Chunk[] { new IconChunk(ChunkSource.None, null, BitmapFontIcon.CrossWorld), new TextChunk(ChunkSource.None, null, world.Value.Name.ExtractText()), }); } LogWindow.DrawChunks(name, false); ImGui.Separator(); var validContentId = chunk.Message?.ContentId is not (null or 0); if (ImGui.Selectable(Language.Context_SendTell)) { // Eureka and Bozja need special handling as tells work different if (Sheets.TerritorySheet.GetRow(Plugin.ClientState.TerritoryType).TerritoryIntendedUse.RowId != 41) { LogWindow.Chat = $"/tell {player.PlayerName}"; if (world.Value.IsPublic) LogWindow.Chat += $"@{world.Value.Name}"; LogWindow.Chat += " "; } else if (validContentId) { LogWindow.Plugin.Functions.Chat.SetEurekaTellChannel(player.PlayerName, world.Value.Name.ToString(), (ushort) world.RowId, 0, chunk.Message!.ContentId, 0, false); } LogWindow.Activate = true; } if (world.Value.IsPublic) { var party = Plugin.PartyList; var leader = (ulong?) party[(int) party.PartyLeaderIndex]?.ContentId; var isLeader = party.Length == 0 || Plugin.ClientState.LocalContentId == leader; var member = party.FirstOrDefault(member => member.Name.TextValue == player.PlayerName && member.World.RowId == world.RowId); var isInParty = member != default; 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); if (isLeader) { if (!isInParty) { if (inInstance && inPartyInstance) { if (validContentId && ImGui.Selectable(Language.Context_InviteToParty)) GameFunctions.Party.InviteInInstance(chunk.Message!.ContentId); } else if (!inInstance && ImGui.BeginMenu(Language.Context_InviteToParty)) { 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); ImGui.EndMenu(); } } if (isInParty && member != null && (!inInstance || (inInstance && inPartyInstance))) { if (ImGui.Selectable(Language.Context_Promote)) GameFunctions.Party.Promote(player.PlayerName, (ulong) member.ContentId); if (ImGui.Selectable(Language.Context_KickFromParty)) GameFunctions.Party.Kick(player.PlayerName, (ulong) member.ContentId); } } var isFriend = GameFunctions.GameFunctions.GetFriends().Any(friend => friend.NameString == player.PlayerName && friend.HomeWorld == world.RowId); if (!isFriend && ImGui.Selectable(Language.Context_SendFriendRequest)) LogWindow.Plugin.Functions.SendFriendRequest(player.PlayerName, (ushort) world.RowId); if (ImGui.Selectable(Language.Context_AddToBlacklist)) LogWindow.Plugin.Functions.AddToBlacklist(player.PlayerName, (ushort) world.RowId); 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)) { LogWindow.SetChannel(inputChannel.Value); LogWindow.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); // View Party Finder 0x2E } 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 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); } } }