feat: add reply in selected chat mode

This commit is contained in:
Anna 2022-01-14 13:25:33 -05:00
parent c5bbd616f0
commit 0f790fa319
Signed by: anna
GPG Key ID: 0B391D8F06FCD9E0
6 changed files with 113 additions and 31 deletions

View File

@ -4,6 +4,7 @@ using Dalamud.Game.Text.SeStringHandling;
namespace ChatTwo; namespace ChatTwo;
internal abstract class Chunk { internal abstract class Chunk {
internal Message? Message { get; set; }
internal SeString? Source { get; set; } internal SeString? Source { get; set; }
internal Payload? Link { get; set; } internal Payload? Link { get; set; }

View File

@ -3,7 +3,7 @@ using ChatTwo.Util;
namespace ChatTwo.Code; namespace ChatTwo.Code;
internal static class ChatTypeExt { internal static class ChatTypeExt {
internal static string? Name(this ChatType type) { internal static string Name(this ChatType type) {
return type switch { return type switch {
ChatType.Debug => "Debug", ChatType.Debug => "Debug",
ChatType.Urgent => "Urgent", ChatType.Urgent => "Urgent",
@ -206,4 +206,33 @@ internal static class ChatTypeExt {
return null; return null;
} }
} }
internal static InputChannel? ToInputChannel(this ChatType type) => type switch {
ChatType.TellOutgoing => InputChannel.Tell,
ChatType.Say => InputChannel.Say,
ChatType.Party => InputChannel.Party,
ChatType.Alliance => InputChannel.Alliance,
ChatType.Yell => InputChannel.Yell,
ChatType.Shout => InputChannel.Shout,
ChatType.FreeCompany => InputChannel.FreeCompany,
ChatType.PvpTeam => InputChannel.PvpTeam,
ChatType.NoviceNetwork => InputChannel.NoviceNetwork,
ChatType.CrossLinkshell1 => InputChannel.CrossLinkshell1,
ChatType.CrossLinkshell2 => InputChannel.CrossLinkshell2,
ChatType.CrossLinkshell3 => InputChannel.CrossLinkshell3,
ChatType.CrossLinkshell4 => InputChannel.CrossLinkshell4,
ChatType.CrossLinkshell5 => InputChannel.CrossLinkshell5,
ChatType.CrossLinkshell6 => InputChannel.CrossLinkshell6,
ChatType.CrossLinkshell7 => InputChannel.CrossLinkshell7,
ChatType.CrossLinkshell8 => InputChannel.CrossLinkshell8,
ChatType.Linkshell1 => InputChannel.Linkshell1,
ChatType.Linkshell2 => InputChannel.Linkshell2,
ChatType.Linkshell3 => InputChannel.Linkshell3,
ChatType.Linkshell4 => InputChannel.Linkshell4,
ChatType.Linkshell5 => InputChannel.Linkshell5,
ChatType.Linkshell6 => InputChannel.Linkshell6,
ChatType.Linkshell7 => InputChannel.Linkshell7,
ChatType.Linkshell8 => InputChannel.Linkshell8,
_ => null,
};
} }

View File

@ -12,20 +12,27 @@ using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using FFXIVClientStructs.FFXIV.Client.UI.Shell; using FFXIVClientStructs.FFXIV.Client.UI.Shell;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using Siggingway; using Siggingway;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
namespace ChatTwo.GameFunctions; namespace ChatTwo.GameFunctions;
internal sealed unsafe class Chat : IDisposable { internal sealed unsafe class Chat : IDisposable {
// Functions
[Signature("E8 ?? ?? ?? ?? 0F B7 44 37 ??", Fallibility = Fallibility.Fallible)] [Signature("E8 ?? ?? ?? ?? 0F B7 44 37 ??", Fallibility = Fallibility.Fallible)]
private readonly delegate* unmanaged<RaptureShellModule*, int, uint, Utf8String*, byte, void> _changeChatChannel = null!; private readonly delegate* unmanaged<RaptureShellModule*, int, uint, Utf8String*, byte, void> _changeChatChannel = null!;
[Signature("4C 8B 81 ?? ?? ?? ?? 4D 85 C0 74 17", Fallibility = Fallibility.Fallible)] [Signature("4C 8B 81 ?? ?? ?? ?? 4D 85 C0 74 17", Fallibility = Fallibility.Fallible)]
private readonly delegate* unmanaged<RaptureLogModule*, uint, ulong> _getContentIdForChatEntry = null!; private readonly delegate* unmanaged<RaptureLogModule*, uint, ulong> _getContentIdForChatEntry = null!;
// Hooks
private delegate byte ChatLogRefreshDelegate(IntPtr log, ushort eventId, AtkValue* value); private delegate byte ChatLogRefreshDelegate(IntPtr log, ushort eventId, AtkValue* value);
private delegate IntPtr ChangeChannelNameDelegate(IntPtr agent); private delegate IntPtr ChangeChannelNameDelegate(IntPtr agent);
private delegate void ReplyInSelectedChatModeDelegate(AgentInterface* agent);
[Signature( [Signature(
"40 53 56 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 49 8B F0 8B FA", "40 53 56 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 49 8B F0 8B FA",
DetourName = nameof(ChatLogRefreshDetour) DetourName = nameof(ChatLogRefreshDetour)
@ -38,6 +45,23 @@ internal sealed unsafe class Chat : IDisposable {
)] )]
private Hook<ChangeChannelNameDelegate>? ChangeChannelNameHook { get; init; } private Hook<ChangeChannelNameDelegate>? ChangeChannelNameHook { get; init; }
[Signature(
"48 89 5C 24 ?? 57 48 83 EC 30 8B B9 ?? ?? ?? ?? 48 8B D9 83 FF FE",
DetourName = nameof(ReplyInSelectedChatModeDetour)
)]
private Hook<ReplyInSelectedChatModeDelegate>? ReplyInSelectedChatModeHook { get; init; }
// Offsets
#pragma warning disable 0649
[Signature("8B B9 ?? ?? ?? ?? 48 8B D9 83 FF FE 0F 84", Offset = 2)]
private readonly int? _replyChannelOffset;
#pragma warning restore 0649
// Events
internal delegate void ChatActivatedEventDelegate(string? input); internal delegate void ChatActivatedEventDelegate(string? input);
internal event ChatActivatedEventDelegate? Activated; internal event ChatActivatedEventDelegate? Activated;
@ -51,6 +75,7 @@ internal sealed unsafe class Chat : IDisposable {
this.ChatLogRefreshHook?.Enable(); this.ChatLogRefreshHook?.Enable();
this.ChangeChannelNameHook?.Enable(); this.ChangeChannelNameHook?.Enable();
this.ReplyInSelectedChatModeHook?.Enable();
this.Plugin.ClientState.Login += this.Login; this.Plugin.ClientState.Login += this.Login;
this.Login(null, null); this.Login(null, null);
@ -59,6 +84,7 @@ internal sealed unsafe class Chat : IDisposable {
public void Dispose() { public void Dispose() {
this.Plugin.ClientState.Login -= this.Login; this.Plugin.ClientState.Login -= this.Login;
this.ReplyInSelectedChatModeHook?.Dispose();
this.ChangeChannelNameHook?.Dispose(); this.ChangeChannelNameHook?.Dispose();
this.ChatLogRefreshHook?.Dispose(); this.ChatLogRefreshHook?.Dispose();
@ -127,27 +153,43 @@ internal sealed unsafe class Chat : IDisposable {
} }
private byte ChatLogRefreshDetour(IntPtr log, ushort eventId, AtkValue* value) { private byte ChatLogRefreshDetour(IntPtr log, ushort eventId, AtkValue* value) {
if (eventId == 0x31 && value != null && value->UInt is 0x05 or 0x0C) { if (eventId != 0x31 || value == null || value->UInt is not (0x05 or 0x0C)) {
string? eventInput = null; return this.ChatLogRefreshHook!.Original(log, eventId, value);
var str = value + 2;
if (str != null && str->String != null) {
var input = MemoryHelper.ReadStringNullTerminated((IntPtr) str->String);
if (input.Length > 0) {
eventInput = input;
}
}
try {
this.Activated?.Invoke(eventInput);
} catch (Exception ex) {
PluginLog.LogError(ex, "Error in ChatActivated event");
}
return 0;
} }
return this.ChatLogRefreshHook!.Original(log, eventId, value); string? eventInput = null;
var str = value + 2;
if (str != null && ((int) str->Type & 0xF) == (int) ValueType.String && str->String != null) {
var input = MemoryHelper.ReadStringNullTerminated((IntPtr) str->String);
if (input.Length > 0) {
eventInput = input;
}
}
try {
this.Activated?.Invoke(eventInput);
} catch (Exception ex) {
PluginLog.LogError(ex, "Error in ChatActivated event");
}
return 0;
}
private void ReplyInSelectedChatModeDetour(AgentInterface* agent) {
if (this._replyChannelOffset == null) {
goto Original;
}
var replyMode = *(int*) ((IntPtr) agent + this._replyChannelOffset.Value);
if (replyMode == -2) {
goto Original;
}
this.SetChannel((InputChannel) replyMode);
Original:
this.ReplyInSelectedChatModeHook!.Original(agent);
} }
internal ulong? GetContentIdForEntry(uint index) { internal ulong? GetContentIdForEntry(uint index) {

View File

@ -15,5 +15,9 @@ internal class Message {
this.Code = code; this.Code = code;
this.Sender = sender; this.Sender = sender;
this.Content = content; this.Content = content;
foreach (var chunk in sender.Concat(content)) {
chunk.Message = this;
}
} }
} }

View File

@ -1,5 +1,6 @@
using System.Numerics; using System.Numerics;
using System.Reflection; using System.Reflection;
using ChatTwo.Code;
using ChatTwo.Ui; using ChatTwo.Ui;
using ChatTwo.Util; using ChatTwo.Util;
using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.SubKinds;
@ -16,7 +17,7 @@ internal sealed class PayloadHandler {
private PluginUi Ui { get; } private PluginUi Ui { get; }
private ChatLog Log { get; } private ChatLog Log { get; }
private HashSet<Payload> PopupPayloads { get; set; } = new(); private HashSet<(Chunk, Payload)> PopupPayloads { get; set; } = new();
private bool _handleTooltips; private bool _handleTooltips;
private uint _hoveredItem; private uint _hoveredItem;
@ -40,8 +41,8 @@ internal sealed class PayloadHandler {
} }
private void DrawPopups() { private void DrawPopups() {
var newPopups = new HashSet<Payload>(); var newPopups = new HashSet<(Chunk, Payload)>();
foreach (var payload in this.PopupPayloads) { foreach (var (chunk, payload) in this.PopupPayloads) {
var id = PopupId(payload); var id = PopupId(payload);
if (id == null) { if (id == null) {
continue; continue;
@ -51,12 +52,12 @@ internal sealed class PayloadHandler {
continue; continue;
} }
newPopups.Add(payload); newPopups.Add((chunk, payload));
ImGui.PushID(id); ImGui.PushID(id);
switch (payload) { switch (payload) {
case PlayerPayload player: { case PlayerPayload player: {
this.DrawPlayerPopup(player); this.DrawPlayerPopup(chunk, player);
break; break;
} }
case ItemPayload item: { case ItemPayload item: {
@ -78,7 +79,7 @@ internal sealed class PayloadHandler {
this.LeftClickPayload(chunk, payload); this.LeftClickPayload(chunk, payload);
break; break;
case ImGuiMouseButton.Right: case ImGuiMouseButton.Right:
this.RightClickPayload(payload); this.RightClickPayload(chunk, payload);
break; break;
} }
} }
@ -211,11 +212,11 @@ internal sealed class PayloadHandler {
} }
} }
private void RightClickPayload(Payload payload) { private void RightClickPayload(Chunk chunk, Payload payload) {
switch (payload) { switch (payload) {
case PlayerPayload: case PlayerPayload:
case ItemPayload: { case ItemPayload: {
this.PopupPayloads.Add(payload); this.PopupPayloads.Add((chunk, payload));
ImGui.OpenPopup(PopupId(payload)); ImGui.OpenPopup(PopupId(payload));
break; break;
} }
@ -267,7 +268,7 @@ internal sealed class PayloadHandler {
} }
} }
private void DrawPlayerPopup(PlayerPayload player) { private void DrawPlayerPopup(Chunk chunk, PlayerPayload player) {
var name = player.PlayerName; var name = player.PlayerName;
if (player.World.IsPublic) { if (player.World.IsPublic) {
name += $"{player.World.Name}"; name += $"{player.World.Name}";
@ -313,6 +314,12 @@ internal sealed class PayloadHandler {
} }
} }
var inputChannel = chunk.Message?.Code.Type.ToInputChannel();
if (inputChannel != null && ImGui.Selectable("Reply in Selected Chat Mode")) {
this.Ui.Plugin.Functions.Chat.SetChannel(inputChannel.Value);
this.Log.Activate = true;
}
if (ImGui.Selectable("Target") && this.FindCharacterForPayload(player) is { } obj) { if (ImGui.Selectable("Target") && this.FindCharacterForPayload(player) is { } obj) {
this.Ui.Plugin.TargetManager.SetTarget(obj); this.Ui.Plugin.TargetManager.SetTarget(obj);
} }
@ -323,7 +330,6 @@ internal sealed class PayloadHandler {
// Add to Blacklist 0x1C // Add to Blacklist 0x1C
// View Party Finder 0x2E // View Party Finder 0x2E
// Reply in Selected Chat Mode 0x64
} }
private PlayerCharacter? FindCharacterForPayload(PlayerPayload payload) { private PlayerCharacter? FindCharacterForPayload(PlayerPayload payload) {

View File

@ -26,7 +26,7 @@ internal sealed class PluginUi : IDisposable {
private (GCHandle, int) _jpFont; private (GCHandle, int) _jpFont;
private (GCHandle, int) _gameSymFont; private (GCHandle, int) _gameSymFont;
private ImVector _ranges; private readonly ImVector _ranges;
private GCHandle _jpRange = GCHandle.Alloc( private GCHandle _jpRange = GCHandle.Alloc(
GlyphRangesJapanese.GlyphRanges, GlyphRangesJapanese.GlyphRanges,