diff --git a/ChatTwo/Code/InputChannelExt.cs b/ChatTwo/Code/InputChannelExt.cs index d313b17..afdcaba 100755 --- a/ChatTwo/Code/InputChannelExt.cs +++ b/ChatTwo/Code/InputChannelExt.cs @@ -1,34 +1,52 @@ namespace ChatTwo.Code; internal static class InputChannelExt { - internal static ChatType ToChatType(this InputChannel input) { - return input switch { - InputChannel.Tell => ChatType.TellOutgoing, - InputChannel.Say => ChatType.Say, - InputChannel.Party => ChatType.Party, - InputChannel.Alliance => ChatType.Alliance, - InputChannel.Yell => ChatType.Yell, - InputChannel.Shout => ChatType.Shout, - InputChannel.FreeCompany => ChatType.FreeCompany, - InputChannel.PvpTeam => ChatType.PvpTeam, - InputChannel.NoviceNetwork => ChatType.NoviceNetwork, - InputChannel.CrossLinkshell1 => ChatType.CrossLinkshell1, - InputChannel.CrossLinkshell2 => ChatType.CrossLinkshell2, - InputChannel.CrossLinkshell3 => ChatType.CrossLinkshell3, - InputChannel.CrossLinkshell4 => ChatType.CrossLinkshell4, - InputChannel.CrossLinkshell5 => ChatType.CrossLinkshell5, - InputChannel.CrossLinkshell6 => ChatType.CrossLinkshell6, - InputChannel.CrossLinkshell7 => ChatType.CrossLinkshell7, - InputChannel.CrossLinkshell8 => ChatType.CrossLinkshell8, - InputChannel.Linkshell1 => ChatType.Linkshell1, - InputChannel.Linkshell2 => ChatType.Linkshell2, - InputChannel.Linkshell3 => ChatType.Linkshell3, - InputChannel.Linkshell4 => ChatType.Linkshell4, - InputChannel.Linkshell5 => ChatType.Linkshell5, - InputChannel.Linkshell6 => ChatType.Linkshell6, - InputChannel.Linkshell7 => ChatType.Linkshell7, - InputChannel.Linkshell8 => ChatType.Linkshell8, - _ => throw new ArgumentOutOfRangeException(nameof(input), input, null), - }; - } + internal static ChatType ToChatType(this InputChannel input) => input switch { + InputChannel.Tell => ChatType.TellOutgoing, + InputChannel.Say => ChatType.Say, + InputChannel.Party => ChatType.Party, + InputChannel.Alliance => ChatType.Alliance, + InputChannel.Yell => ChatType.Yell, + InputChannel.Shout => ChatType.Shout, + InputChannel.FreeCompany => ChatType.FreeCompany, + InputChannel.PvpTeam => ChatType.PvpTeam, + InputChannel.NoviceNetwork => ChatType.NoviceNetwork, + InputChannel.CrossLinkshell1 => ChatType.CrossLinkshell1, + InputChannel.CrossLinkshell2 => ChatType.CrossLinkshell2, + InputChannel.CrossLinkshell3 => ChatType.CrossLinkshell3, + InputChannel.CrossLinkshell4 => ChatType.CrossLinkshell4, + InputChannel.CrossLinkshell5 => ChatType.CrossLinkshell5, + InputChannel.CrossLinkshell6 => ChatType.CrossLinkshell6, + InputChannel.CrossLinkshell7 => ChatType.CrossLinkshell7, + InputChannel.CrossLinkshell8 => ChatType.CrossLinkshell8, + InputChannel.Linkshell1 => ChatType.Linkshell1, + InputChannel.Linkshell2 => ChatType.Linkshell2, + InputChannel.Linkshell3 => ChatType.Linkshell3, + InputChannel.Linkshell4 => ChatType.Linkshell4, + InputChannel.Linkshell5 => ChatType.Linkshell5, + InputChannel.Linkshell6 => ChatType.Linkshell6, + InputChannel.Linkshell7 => ChatType.Linkshell7, + InputChannel.Linkshell8 => ChatType.Linkshell8, + _ => throw new ArgumentOutOfRangeException(nameof(input), input, null), + }; + + public static uint LinkshellIndex(this InputChannel channel) => channel switch { + InputChannel.Linkshell1 => 0, + InputChannel.Linkshell2 => 1, + InputChannel.Linkshell3 => 2, + InputChannel.Linkshell4 => 3, + InputChannel.Linkshell5 => 4, + InputChannel.Linkshell6 => 5, + InputChannel.Linkshell7 => 6, + InputChannel.Linkshell8 => 7, + InputChannel.CrossLinkshell1 => 0, + InputChannel.CrossLinkshell2 => 1, + InputChannel.CrossLinkshell3 => 2, + InputChannel.CrossLinkshell4 => 3, + InputChannel.CrossLinkshell5 => 4, + InputChannel.CrossLinkshell6 => 5, + InputChannel.CrossLinkshell7 => 6, + InputChannel.CrossLinkshell8 => 7, + _ => uint.MaxValue, + }; } diff --git a/ChatTwo/GameFunctions.cs b/ChatTwo/GameFunctions.cs index 1ca63dc..7051170 100755 --- a/ChatTwo/GameFunctions.cs +++ b/ChatTwo/GameFunctions.cs @@ -1,10 +1,15 @@ -using ChatTwo.Code; +using System.Runtime.InteropServices; +using System.Text; +using ChatTwo.Code; +using ChatTwo.Util; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Hooking; using Dalamud.Logging; using Dalamud.Memory; using FFXIVClientStructs.FFXIV.Client.System.Framework; +using FFXIVClientStructs.FFXIV.Client.System.String; using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.FFXIV.Client.UI.Shell; using FFXIVClientStructs.FFXIV.Component.GUI; namespace ChatTwo; @@ -13,21 +18,25 @@ internal unsafe class GameFunctions : IDisposable { private static class Signatures { internal const string ChatLogRefresh = "40 53 56 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 49 8B F0 8B FA"; internal const string ChangeChannelName = "E8 ?? ?? ?? ?? BA ?? ?? ?? ?? 48 8D 4D B0 48 8B F8 E8 ?? ?? ?? ?? 41 8B D6"; + internal const string ChangeChatChannel = "E8 ?? ?? ?? ?? 0F B7 44 37 ??"; } private delegate byte ChatLogRefreshDelegate(IntPtr log, ushort eventId, AtkValue* value); private delegate IntPtr ChangeChannelNameDelegate(IntPtr agent); + private delegate IntPtr ChangeChatChannelDelegate(RaptureShellModule* shell, int channel, uint linkshellIdx, Utf8String* tellTarget, byte one); + internal delegate void ChatActivatedEventDelegate(string? input); private Plugin Plugin { get; } private Hook? ChatLogRefreshHook { get; } private Hook? ChangeChannelNameHook { get; } + private readonly ChangeChatChannelDelegate? _changeChatChannel; internal event ChatActivatedEventDelegate? ChatActivated; - internal (InputChannel channel, string name) ChatChannel { get; private set; } + internal (InputChannel channel, List name) ChatChannel { get; private set; } internal GameFunctions(Plugin plugin) { this.Plugin = plugin; @@ -42,6 +51,10 @@ internal unsafe class GameFunctions : IDisposable { this.ChangeChannelNameHook.Enable(); } + if (this.Plugin.SigScanner.TryScanText(Signatures.ChangeChatChannel, out var changeChannelPtr)) { + this._changeChatChannel = Marshal.GetDelegateForFunctionPointer(changeChannelPtr); + } + this.Plugin.ClientState.Login += this.Login; this.Login(null, null); } @@ -66,6 +79,23 @@ internal unsafe class GameFunctions : IDisposable { this.ChangeChannelNameDetour((IntPtr) agent); } + internal void SetChatChannel(InputChannel channel, string? tellTarget = null) { + if (this._changeChatChannel == null) { + return; + } + + var bytes = Encoding.UTF8.GetBytes(tellTarget ?? ""); + var target = new Utf8String(); + fixed (byte* tellTargetPtr = bytes) { + var zero = stackalloc byte[1]; + zero[0] = 0; + + target.StringPtr = tellTargetPtr == null ? zero : tellTargetPtr; + target.StringLength = bytes.Length; + this._changeChatChannel(RaptureShellModule.Instance, (int) (channel + 1), channel.LinkshellIndex(), &target, 1); + } + } + internal static void SetAddonInteractable(string name, bool interactable) { var unitManager = AtkStage.GetSingleton()->RaptureAtkUnitManager; @@ -217,7 +247,12 @@ internal unsafe class GameFunctions : IDisposable { return ret; } - this.ChatChannel = ((InputChannel) channel, name.TextValue.TrimStart('\uE01E').Trim()); + var nameChunks = ChunkUtil.ToChunks(name, null).ToList(); + if (nameChunks.Count > 0 && nameChunks[0] is TextChunk text) { + text.Content = text.Content.TrimStart('\uE01E').TrimStart(); + } + + this.ChatChannel = ((InputChannel) channel, nameChunks); return ret; } diff --git a/ChatTwo/PayloadHandler.cs b/ChatTwo/PayloadHandler.cs index 251514a..366fade 100755 --- a/ChatTwo/PayloadHandler.cs +++ b/ChatTwo/PayloadHandler.cs @@ -7,6 +7,7 @@ using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Logging; using Dalamud.Utility; using ImGuiNET; +using ImGuiScene; namespace ChatTwo; @@ -109,7 +110,20 @@ internal sealed class PayloadHandler { } } + private static void InlineIcon(TextureWrap icon) { + var lineHeight = ImGui.CalcTextSize("A").Y; + + var cursor = ImGui.GetCursorPos(); + ImGui.Image(icon.ImGuiHandle, new Vector2(icon.Width, icon.Height)); + ImGui.SameLine(); + ImGui.SetCursorPos(cursor + new Vector2(icon.Width + 4, (float) icon.Height / 2 - lineHeight / 2)); + } + private void HoverStatus(StatusPayload status) { + if (this.Ui.Plugin.TextureCache.GetStatus(status.Status) is { } icon) { + InlineIcon(icon); + } + var name = ChunkUtil.ToChunks(status.Status.Name.ToDalamudString(), null); this.Log.DrawChunks(name.ToList()); ImGui.Separator(); @@ -119,6 +133,10 @@ internal sealed class PayloadHandler { } private void HoverItem(ItemPayload item) { + if (this.Ui.Plugin.TextureCache.GetItem(item.Item) is { } icon) { + InlineIcon(icon); + } + var name = ChunkUtil.ToChunks(item.Item.Name.ToDalamudString(), null); this.Log.DrawChunks(name.ToList()); ImGui.Separator(); diff --git a/ChatTwo/Plugin.cs b/ChatTwo/Plugin.cs index bedcb6a..5156c5d 100755 --- a/ChatTwo/Plugin.cs +++ b/ChatTwo/Plugin.cs @@ -46,6 +46,7 @@ public sealed class Plugin : IDalamudPlugin { internal Configuration Config { get; } internal XivCommonBase Common { get; } + internal TextureCache TextureCache { get; } internal GameFunctions Functions { get; } internal Store Store { get; } internal PluginUi Ui { get; } @@ -54,6 +55,7 @@ public sealed class Plugin : IDalamudPlugin { public Plugin() { this.Config = this.Interface!.GetPluginConfig() as Configuration ?? new Configuration(); this.Common = new XivCommonBase(); + this.TextureCache = new TextureCache(this.DataManager!); this.Functions = new GameFunctions(this); this.Store = new Store(this); this.Ui = new PluginUi(this); @@ -69,6 +71,7 @@ public sealed class Plugin : IDalamudPlugin { this.Ui.Dispose(); this.Store.Dispose(); this.Functions.Dispose(); + this.TextureCache.Dispose(); this.Common.Dispose(); } diff --git a/ChatTwo/PluginUi.cs b/ChatTwo/PluginUi.cs index 97fac2f..dc2119a 100755 --- a/ChatTwo/PluginUi.cs +++ b/ChatTwo/PluginUi.cs @@ -9,6 +9,9 @@ namespace ChatTwo; internal sealed class PluginUi : IDisposable { internal Plugin Plugin { get; } + + internal bool SettingsVisible; + internal ImFontPtr? RegularFont { get; private set; } internal ImFontPtr? ItalicFont { get; private set; } internal Vector4 DefaultText { get; private set; } @@ -55,7 +58,7 @@ internal sealed class PluginUi : IDisposable { var builder = new ImFontGlyphRangesBuilderPtr(ImGuiNative.ImFontGlyphRangesBuilder_ImFontGlyphRangesBuilder()); builder.AddRanges(ImGui.GetIO().Fonts.GetGlyphRangesDefault()); - builder.AddText("←→↑↓《》■※☀★★☆♥♡ヅツッシ☀☁☂℃℉°♀♂♠♣♦♣♧®©™€$£♯♭♪✓√◎◆◇♦■□〇●△▽▼▲‹›≤≥<«“”─"); + builder.AddText("←→↑↓《》■※☀★★☆♥♡ヅツッシ☀☁☂℃℉°♀♂♠♣♦♣♧®©™€$£♯♭♪✓√◎◆◇♦■□〇●△▽▼▲‹›≤≥<«“”─\~"); builder.BuildRanges(out this._ranges); var regular = this.GetResource("ChatTwo.fonts.NotoSans-Regular.ttf"); diff --git a/ChatTwo/TextureCache.cs b/ChatTwo/TextureCache.cs new file mode 100755 index 0000000..138088a --- /dev/null +++ b/ChatTwo/TextureCache.cs @@ -0,0 +1,59 @@ +using Dalamud.Data; +using ImGuiScene; +using Lumina.Excel.GeneratedSheets; + +namespace ChatTwo; + +internal class TextureCache : IDisposable { + private DataManager Data { get; } + + private readonly Dictionary _itemIcons = new(); + private readonly Dictionary _statusIcons = new(); + + internal IReadOnlyDictionary ItemIcons => this._itemIcons; + internal IReadOnlyDictionary StatusIcons => this._statusIcons; + + internal TextureCache(DataManager data) { + this.Data = data; + } + + public void Dispose() { + var allIcons = this.ItemIcons.Values + .Concat(this.StatusIcons.Values); + + foreach (var tex in allIcons) { + tex.Dispose(); + } + } + + private void AddIcon(IDictionary dict, uint icon) { + if (dict.ContainsKey(icon)) { + return; + } + + var tex = this.Data.GetImGuiTextureIcon(icon); + if (tex != null) { + dict[icon] = tex; + } + } + + internal void AddItem(Item item) { + this.AddIcon(this._itemIcons, item.Icon); + } + + internal void AddStatus(Status status) { + this.AddIcon(this._statusIcons, status.Icon); + } + + internal TextureWrap? GetItem(Item item) { + this.AddItem(item); + this.ItemIcons.TryGetValue(item.Icon, out var icon); + return icon; + } + + internal TextureWrap? GetStatus(Status status) { + this.AddStatus(status); + this.StatusIcons.TryGetValue(status.Icon, out var icon); + return icon; + } +} diff --git a/ChatTwo/Ui/ChatLog.cs b/ChatTwo/Ui/ChatLog.cs index 2ae8b4f..6ea3054 100755 --- a/ChatTwo/Ui/ChatLog.cs +++ b/ChatTwo/Ui/ChatLog.cs @@ -1,12 +1,16 @@ using System.Numerics; using ChatTwo.Code; using ChatTwo.Util; +using Dalamud.Interface; using ImGuiNET; using ImGuiScene; +using Lumina.Excel.GeneratedSheets; namespace ChatTwo.Ui; internal sealed class ChatLog : IUiComponent { + private const string ChatChannelPicker = "chat-channel-picker"; + private PluginUi Ui { get; } internal bool Activate; @@ -143,7 +147,39 @@ internal sealed class ChatLog : IUiComponent { ImGui.SetKeyboardFocusHere(); } - ImGui.TextUnformatted(this.Ui.Plugin.Functions.ChatChannel.name); + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + try { + this.DrawChunks(this.Ui.Plugin.Functions.ChatChannel.name); + } finally { + ImGui.PopStyleVar(); + } + + var beforeIcon = ImGui.GetCursorPos(); + + if (ImGuiUtil.IconButton(FontAwesomeIcon.Comment)) { + ImGui.OpenPopup(ChatChannelPicker); + } + + if (ImGui.BeginPopup(ChatChannelPicker)) { + foreach (var channel in Enum.GetValues()) { + var name = this.Ui.Plugin.DataManager.GetExcelSheet()! + .FirstOrDefault(row => row.LogKind == (byte) channel.ToChatType()) + ?.Name + ?.RawString ?? channel.ToString(); + + if (ImGui.Selectable(name)) { + this.Ui.Plugin.Functions.SetChatChannel(channel); + } + } + + ImGui.EndPopup(); + } + + ImGui.SameLine(); + var afterIcon = ImGui.GetCursorPos(); + + var buttonWidth = afterIcon.X - beforeIcon.X; + var inputWidth = ImGui.GetContentRegionAvail().X - buttonWidth; var inputType = this.Ui.Plugin.Functions.ChatChannel.channel.ToChatType(); var inputColour = this.Ui.Plugin.Config.ChatColours.TryGetValue(inputType, out var inputCol) @@ -154,7 +190,7 @@ internal sealed class ChatLog : IUiComponent { ImGui.PushStyleColor(ImGuiCol.Text, ColourUtil.RgbaToAbgr(inputColour.Value)); } - ImGui.SetNextItemWidth(-1); + ImGui.SetNextItemWidth(inputWidth); const ImGuiInputTextFlags inputFlags = ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.CallbackAlways | ImGuiInputTextFlags.CallbackHistory; @@ -174,6 +210,12 @@ internal sealed class ChatLog : IUiComponent { ImGui.PopStyleColor(); } + ImGui.SameLine(); + + if (ImGuiUtil.IconButton(FontAwesomeIcon.Cog)) { + this.Ui.SettingsVisible ^= true; + } + ImGui.End(); } diff --git a/ChatTwo/Ui/Settings.cs b/ChatTwo/Ui/Settings.cs index 12269fd..103edba 100755 --- a/ChatTwo/Ui/Settings.cs +++ b/ChatTwo/Ui/Settings.cs @@ -9,8 +9,6 @@ namespace ChatTwo.Ui; internal sealed class Settings : IUiComponent { private PluginUi Ui { get; } - private bool _visible; - private bool _hideChat; private bool _nativeItemTooltips; private float _fontSize; @@ -29,7 +27,7 @@ internal sealed class Settings : IUiComponent { } private void Command(string command, string args) { - this._visible ^= true; + this.Ui.SettingsVisible ^= true; } private void Initialise() { @@ -42,11 +40,11 @@ internal sealed class Settings : IUiComponent { } public void Draw() { - if (!this._visible) { + if (!this.Ui.SettingsVisible) { return; } - if (!ImGui.Begin($"{this.Ui.Plugin.Name} settings", ref this._visible)) { + if (!ImGui.Begin($"{this.Ui.Plugin.Name} settings", ref this.Ui.SettingsVisible)) { ImGui.End(); return; } @@ -148,13 +146,13 @@ internal sealed class Settings : IUiComponent { if (ImGui.Button("Save and close")) { save = true; - this._visible = false; + this.Ui.SettingsVisible = false; } ImGui.SameLine(); if (ImGui.Button("Discard")) { - this._visible = false; + this.Ui.SettingsVisible = false; } ImGui.End(); diff --git a/ChatTwo/Util/ImGuiUtil.cs b/ChatTwo/Util/ImGuiUtil.cs index eb23b09..0b1ea39 100755 --- a/ChatTwo/Util/ImGuiUtil.cs +++ b/ChatTwo/Util/ImGuiUtil.cs @@ -1,5 +1,6 @@ using System.Text; using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Interface; using ImGuiNET; namespace ChatTwo.Util; @@ -34,14 +35,20 @@ internal static class ImGuiUtil { PostPayload(payload, handler); } + if (csText.Length == 0) { + return; + } + foreach (var part in csText.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None)) { var bytes = Encoding.UTF8.GetBytes(part); fixed (byte* rawText = bytes) { var text = rawText; var textEnd = text + bytes.Length; - // idk how this is possible, but it is, I guess + // empty string if (text == null) { + ImGui.TextUnformatted(""); + ImGui.TextUnformatted(""); return; } @@ -63,6 +70,8 @@ internal static class ImGuiUtil { endPrevLine = ImGuiNative.ImFont_CalcWordWrapPositionA(ImGui.GetFont().NativePtr, scale, text, textEnd, widthLeft); if (endPrevLine == null) { + ImGui.TextUnformatted(""); + ImGui.TextUnformatted(""); break; } @@ -71,4 +80,19 @@ internal static class ImGuiUtil { } } } + + internal static bool IconButton(FontAwesomeIcon icon, string? id = null) { + ImGui.PushFont(UiBuilder.IconFont); + + var label = icon.ToIconString(); + if (id != null) { + label += $"##{id}"; + } + + var ret = ImGui.Button(label); + + ImGui.PopFont(); + + return ret; + } }