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;
internal abstract class Chunk {
internal Message? Message { get; set; }
internal SeString? Source { get; set; }
internal Payload? Link { get; set; }

View File

@ -3,7 +3,7 @@ using ChatTwo.Util;
namespace ChatTwo.Code;
internal static class ChatTypeExt {
internal static string? Name(this ChatType type) {
internal static string Name(this ChatType type) {
return type switch {
ChatType.Debug => "Debug",
ChatType.Urgent => "Urgent",
@ -206,4 +206,33 @@ internal static class ChatTypeExt {
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.Component.GUI;
using Siggingway;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
namespace ChatTwo.GameFunctions;
internal sealed unsafe class Chat : IDisposable {
// Functions
[Signature("E8 ?? ?? ?? ?? 0F B7 44 37 ??", Fallibility = Fallibility.Fallible)]
private readonly delegate* unmanaged<RaptureShellModule*, int, uint, Utf8String*, byte, void> _changeChatChannel = null!;
[Signature("4C 8B 81 ?? ?? ?? ?? 4D 85 C0 74 17", Fallibility = Fallibility.Fallible)]
private readonly delegate* unmanaged<RaptureLogModule*, uint, ulong> _getContentIdForChatEntry = null!;
// Hooks
private delegate byte ChatLogRefreshDelegate(IntPtr log, ushort eventId, AtkValue* value);
private delegate IntPtr ChangeChannelNameDelegate(IntPtr agent);
private delegate void ReplyInSelectedChatModeDelegate(AgentInterface* agent);
[Signature(
"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)
@ -38,6 +45,23 @@ internal sealed unsafe class Chat : IDisposable {
)]
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 event ChatActivatedEventDelegate? Activated;
@ -51,6 +75,7 @@ internal sealed unsafe class Chat : IDisposable {
this.ChatLogRefreshHook?.Enable();
this.ChangeChannelNameHook?.Enable();
this.ReplyInSelectedChatModeHook?.Enable();
this.Plugin.ClientState.Login += this.Login;
this.Login(null, null);
@ -59,6 +84,7 @@ internal sealed unsafe class Chat : IDisposable {
public void Dispose() {
this.Plugin.ClientState.Login -= this.Login;
this.ReplyInSelectedChatModeHook?.Dispose();
this.ChangeChannelNameHook?.Dispose();
this.ChatLogRefreshHook?.Dispose();
@ -127,27 +153,43 @@ internal sealed unsafe class Chat : IDisposable {
}
private byte ChatLogRefreshDetour(IntPtr log, ushort eventId, AtkValue* value) {
if (eventId == 0x31 && value != null && value->UInt is 0x05 or 0x0C) {
string? eventInput = null;
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;
if (eventId != 0x31 || value == null || value->UInt is not (0x05 or 0x0C)) {
return this.ChatLogRefreshHook!.Original(log, eventId, value);
}
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) {

View File

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

View File

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

View File

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