diff --git a/ChatTwo/Code/InputChannelExt.cs b/ChatTwo/Code/InputChannelExt.cs index 0a3161d..9f429a8 100755 --- a/ChatTwo/Code/InputChannelExt.cs +++ b/ChatTwo/Code/InputChannelExt.cs @@ -126,4 +126,28 @@ internal static class InputChannelExt { .Where(id => id != null) .Cast(); } + + internal static bool IsLinkshell(this InputChannel channel) => channel switch { + InputChannel.Linkshell1 => true, + InputChannel.Linkshell2 => true, + InputChannel.Linkshell3 => true, + InputChannel.Linkshell4 => true, + InputChannel.Linkshell5 => true, + InputChannel.Linkshell6 => true, + InputChannel.Linkshell7 => true, + InputChannel.Linkshell8 => true, + _ => false, + }; + + internal static bool IsCrossLinkshell(this InputChannel channel) => channel switch { + InputChannel.CrossLinkshell1 => true, + InputChannel.CrossLinkshell2 => true, + InputChannel.CrossLinkshell3 => true, + InputChannel.CrossLinkshell4 => true, + InputChannel.CrossLinkshell5 => true, + InputChannel.CrossLinkshell6 => true, + InputChannel.CrossLinkshell7 => true, + InputChannel.CrossLinkshell8 => true, + _ => false, + }; } diff --git a/ChatTwo/GameFunctions/Chat.cs b/ChatTwo/GameFunctions/Chat.cs index 4ad6571..7cd3705 100755 --- a/ChatTwo/GameFunctions/Chat.cs +++ b/ChatTwo/GameFunctions/Chat.cs @@ -9,6 +9,7 @@ using Dalamud.Logging; using Dalamud.Memory; using FFXIVClientStructs.FFXIV.Client.System.Framework; using FFXIVClientStructs.FFXIV.Client.System.String; +using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Misc; using FFXIVClientStructs.FFXIV.Client.UI.Shell; @@ -46,7 +47,13 @@ internal sealed unsafe class Chat : IDisposable { private readonly delegate* unmanaged _getNetworkModule = null!; [Signature("E8 ?? ?? ?? ?? 48 8B C8 E8 ?? ?? ?? ?? 45 8D 46 FB")] - private readonly delegate* unmanaged _getLinkshellName = null!; + private readonly delegate* unmanaged _getCrossLinkshellName = null!; + + [Signature("3B 51 10 73 0F 8B C2 48 83 C0 0B")] + private readonly delegate* unmanaged _getLinkshellInfo = null!; + + [Signature("E8 ?? ?? ?? ?? 4C 8B C0 FF C3")] + private readonly delegate* unmanaged _getLinkshellName = null!; // Hooks @@ -92,6 +99,9 @@ internal sealed unsafe class Chat : IDisposable { [Signature("89 83 ?? ?? ?? ?? 48 8B 01 83 FE 13 7C 05 41 8B D4 EB 03 83 CA FF FF 90", Offset = 2)] private readonly int? _shellChannelOffset; + [Signature("4C 8D B6 ?? ?? ?? ?? 41 8B 1E 45 85 E4 74 7A 33 FF 8B EF 66 0F 1F 44 00", Offset = 3)] + private readonly int? _linkshellCycleOffset; + #pragma warning restore 0649 // Events @@ -130,15 +140,54 @@ internal sealed unsafe class Chat : IDisposable { } internal string? GetLinkshellName(uint idx) { + var infoProxy = this.Plugin.Functions.GetInfoProxyByIndex(2); + if (infoProxy == IntPtr.Zero) { + return null; + } + + var lsInfo = this._getLinkshellInfo(infoProxy, idx); + if (lsInfo == null) { + return null; + } + + var utf = this._getLinkshellName(infoProxy, *lsInfo); + return utf == null ? null : MemoryHelper.ReadStringNullTerminated((IntPtr) utf); + } + + internal string? GetCrossLinkshellName(uint idx) { var infoProxy = this.Plugin.Functions.GetInfoProxyByIndex(26); if (infoProxy == IntPtr.Zero) { return null; } - var utf = this._getLinkshellName(infoProxy, idx); + var utf = this._getCrossLinkshellName(infoProxy, idx); return utf == null ? null : utf->ToString(); } + internal ulong RotateLinkshellHistory(RotateMode mode) { + if (mode == RotateMode.None && this._linkshellCycleOffset != null) { + // for the branch at 6.08: 5E1680 + var uiModule = (IntPtr) Framework.Instance()->GetUiModule(); + *(int*) (uiModule + this._linkshellCycleOffset.Value) = -1; + } + + return RotateLinkshellHistoryInternal(189, mode); + } + + internal ulong RotateCrossLinkshellHistory(RotateMode mode) => RotateLinkshellHistoryInternal(191, mode); + + private static ulong RotateLinkshellHistoryInternal(int vfunc, RotateMode mode) { + var idx = mode switch { + RotateMode.Forward => 1, + RotateMode.Reverse => -1, + _ => 0, + }; + + var uiModule = Framework.Instance()->GetUiModule(); + var cycleFunc = (delegate* unmanaged) uiModule->vfunc[vfunc]; + return cycleFunc(uiModule, idx); + } + private readonly Dictionary _keybinds = new(); internal IReadOnlyDictionary Keybinds => this._keybinds; diff --git a/ChatTwo/Ui/ChatLog.cs b/ChatTwo/Ui/ChatLog.cs index e4841bc..3efd1a1 100755 --- a/ChatTwo/Ui/ChatLog.cs +++ b/ChatTwo/Ui/ChatLog.cs @@ -27,7 +27,6 @@ internal sealed class ChatLog : IUiComponent { private int _lastTab; private InputChannel? _tempChannel; private TellTarget? _tellTarget; - private int _tellIdx; private PayloadHandler PayloadHandler { get; } private Dictionary TextCommandChannels { get; } = new(); @@ -70,18 +69,17 @@ internal sealed class ChatLog : IUiComponent { if (info.Channel is InputChannel.Tell) { if (info.Rotate != RotateMode.None) { - this._tellIdx = prevTemp != InputChannel.Tell + var idx = prevTemp != InputChannel.Tell ? 0 : info.Rotate == RotateMode.Reverse ? -1 : 1; - var tellInfo = this.Ui.Plugin.Functions.Chat.GetTellHistoryInfo(this._tellIdx); + var tellInfo = this.Ui.Plugin.Functions.Chat.GetTellHistoryInfo(idx); if (tellInfo != null && reason != null) { this._tellTarget = new TellTarget(tellInfo.Name, (ushort) tellInfo.World, tellInfo.ContentId, reason.Value); } } else { - this._tellIdx = 0; this._tellTarget = null; if (target != null) { @@ -89,9 +87,20 @@ internal sealed class ChatLog : IUiComponent { } } } else { - this._tellIdx = 0; this._tellTarget = null; } + + var mode = prevTemp == null + ? RotateMode.None + : info.Rotate; + + if (info.Channel is InputChannel.Linkshell1 && info.Rotate != RotateMode.None) { + var idx = this.Ui.Plugin.Functions.Chat.RotateLinkshellHistory(mode); + this._tempChannel = info.Channel.Value + (uint) idx; + } else if (info.Channel is InputChannel.CrossLinkshell1 && info.Rotate != RotateMode.None) { + var idx = this.Ui.Plugin.Functions.Chat.RotateCrossLinkshellHistory(mode); + this._tempChannel = info.Channel.Value + (uint) idx; + } } if (info.Text != null && this.Chat.Length == 0) { @@ -266,19 +275,27 @@ internal sealed class ChatLog : IUiComponent { ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero); try { - if (this._tempChannel != null) { - if (this._tellTarget != null) { - var world = this.Ui.Plugin.DataManager.GetExcelSheet() - ?.GetRow(this._tellTarget.World) - ?.Name - ?.RawString ?? "???"; + if (this._tellTarget != null) { + var world = this.Ui.Plugin.DataManager.GetExcelSheet() + ?.GetRow(this._tellTarget.World) + ?.Name + ?.RawString ?? "???"; - this.DrawChunks(new List { - new TextChunk(null, null, "Tell "), - new TextChunk(null, null, this._tellTarget.Name), - new IconChunk(null, null, BitmapFontIcon.CrossWorld), - new TextChunk(null, null, world), - }); + this.DrawChunks(new Chunk[] { + new TextChunk(null, null, "Tell "), + new TextChunk(null, null, this._tellTarget.Name), + new IconChunk(null, null, BitmapFontIcon.CrossWorld), + new TextChunk(null, null, world), + }); + } else if (this._tempChannel != null) { + if (this._tempChannel.Value.IsLinkshell()) { + var idx = (uint) this._tempChannel.Value - (uint) InputChannel.Linkshell1; + var lsName = this.Ui.Plugin.Functions.Chat.GetLinkshellName(idx); + ImGui.TextUnformatted($"LS #{idx + 1}: {lsName}"); + } else if (this._tempChannel.Value.IsCrossLinkshell()) { + var idx = (uint) this._tempChannel.Value - (uint) InputChannel.CrossLinkshell1; + var cwlsName = this.Ui.Plugin.Functions.Chat.GetCrossLinkshellName(idx); + ImGui.TextUnformatted($"CWLS [{idx + 1}]: {cwlsName}"); } else { ImGui.TextUnformatted(this._tempChannel.Value.ToChatType().Name()); } @@ -312,7 +329,6 @@ internal sealed class ChatLog : IUiComponent { if (ImGui.Selectable(name)) { this.Ui.Plugin.Functions.Chat.SetChannel(channel); - this._tellIdx = 0; this._tellTarget = null; } } @@ -394,7 +410,6 @@ internal sealed class ChatLog : IUiComponent { } this._tempChannel = null; - this._tellIdx = 0; } if (inputColour != null) {