Compare commits

..

No commits in common. "0f790fa319affcfb82f9e2d4628ca3ff9e63b0e5" and "cd85e579401ac444534fd758cc1405a27c836df0" have entirely different histories.

14 changed files with 561 additions and 711 deletions

View File

@ -4,7 +4,6 @@ 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,33 +206,4 @@ 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,
};
} }

523
ChatTwo/GameFunctions.cs Executable file
View File

@ -0,0 +1,523 @@
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;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using FFXIVClientStructs.FFXIV.Client.UI.Shell;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Lumina.Excel.GeneratedSheets;
using Siggingway;
namespace ChatTwo;
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 IsMentorA1 = "48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 84 C0 74 71 0F B6 86";
internal const string ResolveTextCommandPlaceholder = "E8 ?? ?? ?? ?? 49 8D 4F 18 4C 8B E0";
internal const string CurrentChatEntryOffset = "8B 77 ?? 8D 46 01 89 47 14 81 FE ?? ?? ?? ?? 72 03 FF 47";
}
#region 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!;
[Signature("E8 ?? ?? ?? ?? 8B FD 8B CD", Fallibility = Fallibility.Fallible)]
private readonly delegate* unmanaged<IntPtr, uint, IntPtr> _indexer = null!;
[Signature("E8 ?? ?? ?? ?? 33 C0 EB 51", Fallibility = Fallibility.Fallible)]
private readonly delegate* unmanaged<IntPtr, ulong, byte*, ushort, void> _inviteToParty = null!;
[Signature("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 48 8B CB E8 ?? ?? ?? ?? 45 33 C9", Fallibility = Fallibility.Fallible)]
private readonly delegate* unmanaged<IntPtr, ulong, ushort, byte*, byte> _inviteToNoviceNetwork = null!;
[Signature("E8 ?? ?? ?? ?? EB 35 BA", Fallibility = Fallibility.Fallible)]
private readonly delegate* unmanaged<uint, uint, ulong, uint, byte, byte> _tryOn = null!;
[Signature("E8 ?? ?? ?? ?? EB 7B 49 8B 06", Fallibility = Fallibility.Fallible)]
private readonly delegate* unmanaged<AgentInterface*, uint, void> _linkItem = null!;
[Signature("E8 ?? ?? ?? ?? EB 3F 83 F8 FE", Fallibility = Fallibility.Fallible)]
private readonly delegate* unmanaged<AgentInterface*, ushort, uint, byte, void> _itemComparison = null!;
[Signature("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 41 B4 01", Fallibility = Fallibility.Fallible)]
private readonly delegate* unmanaged<IntPtr, uint, void> _searchForRecipesUsingItem = null!;
[Signature("E8 ?? ?? ?? ?? EB 45 45 33 C9", Fallibility = Fallibility.Fallible)]
private readonly delegate* unmanaged<void*, uint, byte, void> _searchForItem = null!;
[Signature("E8 ?? ?? ?? ?? 84 C0 74 0D B0 02", Fallibility = Fallibility.Fallible)]
private readonly delegate* unmanaged<IntPtr, byte> _isMentor = null!;
[Signature("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 49 8B 56 20", Fallibility = Fallibility.Fallible)]
private readonly delegate* unmanaged<AgentInterface*, byte*, ushort, ulong, void> _promote = null!;
[Signature("E8 ?? ?? ?? ?? EB 66 49 8B 4E 20", Fallibility = Fallibility.Fallible)]
private readonly delegate* unmanaged<AgentInterface*, byte*, ushort, ulong, void> _kick = null!;
#endregion
#region Hooks
private delegate byte ChatLogRefreshDelegate(IntPtr log, ushort eventId, AtkValue* value);
private delegate IntPtr ChangeChannelNameDelegate(IntPtr agent);
private delegate IntPtr ResolveTextCommandPlaceholderDelegate(IntPtr a1, byte* placeholderText, byte a3, byte a4);
[Signature(Signatures.ChatLogRefresh, DetourName = nameof(ChatLogRefreshDetour))]
private Hook<ChatLogRefreshDelegate>? ChatLogRefreshHook { get; init; }
[Signature(Signatures.ChangeChannelName, DetourName = nameof(ChangeChannelNameDetour))]
private Hook<ChangeChannelNameDelegate>? ChangeChannelNameHook { get; init; }
[Signature(Signatures.ResolveTextCommandPlaceholder, DetourName = nameof(ResolveTextCommandPlaceholderDetour))]
private Hook<ResolveTextCommandPlaceholderDelegate>? ResolveTextCommandPlaceholderHook { get; init; }
#endregion
#pragma warning disable 0649
[Signature(Signatures.CurrentChatEntryOffset, Offset = 2)]
private readonly byte? _currentChatEntryOffset;
[Signature(Signatures.IsMentorA1, ScanType = ScanType.StaticAddress)]
private readonly IntPtr? _isMentorA1;
#pragma warning restore 0649
internal const int HqItemOffset = 1_000_000;
private Plugin Plugin { get; }
internal delegate void ChatActivatedEventDelegate(string? input);
internal event ChatActivatedEventDelegate? ChatActivated;
internal (InputChannel channel, List<Chunk> name) ChatChannel { get; private set; }
internal GameFunctions(Plugin plugin) {
this.Plugin = plugin;
Siggingway.Siggingway.Initialise(this.Plugin.SigScanner, this);
this.ChatLogRefreshHook?.Enable();
this.ChangeChannelNameHook?.Enable();
this.ResolveTextCommandPlaceholderHook?.Enable();
this.Plugin.ClientState.Login += this.Login;
this.Login(null, null);
}
public void Dispose() {
this.Plugin.ClientState.Login -= this.Login;
this.ResolveTextCommandPlaceholderHook?.Dispose();
this.ChangeChannelNameHook?.Dispose();
this.ChatLogRefreshHook?.Dispose();
this.ChatActivated = null;
Marshal.FreeHGlobal(this._placeholderNamePtr);
}
private void Login(object? sender, EventArgs? e) {
if (this.ChangeChannelNameHook == null) {
return;
}
var agent = Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.ChatLog);
if (agent == null) {
return;
}
this.ChangeChannelNameDetour((IntPtr) agent);
}
internal uint? GetCurrentChatLogEntryIndex() {
if (this._currentChatEntryOffset == null) {
return null;
}
var log = (IntPtr) Framework.Instance()->GetUiModule()->GetRaptureLogModule();
return *(uint*) (log + this._currentChatEntryOffset.Value);
}
internal ulong? GetContentIdForChatLogEntry(uint index) {
if (this._getContentIdForChatEntry == null) {
return null;
}
return this._getContentIdForChatEntry(Framework.Instance()->GetUiModule()->GetRaptureLogModule(), index);
}
internal void InviteToParty(string name, ushort world) {
if (this._inviteToParty == null || this._indexer == null) {
return;
}
var uiModule = Framework.Instance()->GetUiModule();
// 6.05: 20D722
var func = (delegate*<UIModule*, IntPtr>) uiModule->vfunc[33];
var toIndex = func(uiModule);
var a1 = this._indexer(toIndex, 1);
fixed (byte* namePtr = name.ToTerminatedBytes()) {
// can specify content id if we have it, but there's no need
this._inviteToParty(a1, 0, namePtr, world);
}
}
internal void SendFriendRequest(string name, ushort world) {
var row = this.Plugin.DataManager.GetExcelSheet<World>()!.GetRow(world);
if (row == null) {
return;
}
var worldName = row.Name.RawString;
this._replacementName = $"{name}@{worldName}";
this.Plugin.Common.Functions.Chat.SendMessage($"/friendlist add {this._placeholder}");
}
internal void InviteToNoviceNetwork(string name, ushort world) {
if (this._inviteToNoviceNetwork == null || this._indexer == null) {
return;
}
var uiModule = Framework.Instance()->GetUiModule();
// 6.05: 20D722
var func = (delegate*<UIModule*, IntPtr>) uiModule->vfunc[33];
var toIndex = func(uiModule);
// 6.05: 20E4CB
var a1 = this._indexer(toIndex, 0x11);
fixed (byte* namePtr = name.ToTerminatedBytes()) {
// can specify content id if we have it, but there's no need
this._inviteToNoviceNetwork(a1, 0, world, namePtr);
}
}
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;
var addon = (IntPtr) unitManager->GetAddonByName(name);
if (addon == IntPtr.Zero) {
return;
}
var flags = (uint*) (addon + 0x180);
if (interactable) {
*flags &= ~(1u << 22);
} else {
*flags |= 1 << 22;
}
}
internal static void SetChatInteractable(bool interactable) {
for (var i = 0; i < 4; i++) {
SetAddonInteractable($"ChatLogPanel_{i}", interactable);
}
SetAddonInteractable("ChatLog", interactable);
}
internal static bool IsAddonInteractable(string name) {
var unitManager = AtkStage.GetSingleton()->RaptureAtkUnitManager;
var addon = (IntPtr) unitManager->GetAddonByName(name);
if (addon == IntPtr.Zero) {
return false;
}
var flags = (uint*) (addon + 0x180);
return (*flags & (1 << 22)) == 0;
}
internal static void OpenItemTooltip(uint id) {
var atkStage = AtkStage.GetSingleton();
var agent = Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.ItemDetail);
var addon = atkStage->RaptureAtkUnitManager->GetAddonByName("ItemDetail");
if (agent == null || addon == null) {
// atkStage ain't gonna be null or we have bigger problems
return;
}
var agentPtr = (IntPtr) agent;
// addresses mentioned here are 6.01
// see the call near the end of AgentItemDetail.Update
// offsets valid as of 6.01
// 8BFC49: sets some shit
*(uint*) (agentPtr + 0x20) = 22;
// 8C04C8: switch goes down to default, which is what we want
*(byte*) (agentPtr + 0x118) = 1;
// 8BFCF6: item id when hovering over item in chat
*(uint*) (agentPtr + 0x11C) = id;
// 8BFCE4: always 0 when hovering over item in chat
*(uint*) (agentPtr + 0x120) = 0;
// 8C0B55: skips a check to do with inventory
*(byte*) (agentPtr + 0x128) &= 0xEF;
// 8BFC7C: when set to 1, lets everything continue (one frame)
*(byte*) (agentPtr + 0x146) = 1;
// 8BFC89: skips early return
*(byte*) (agentPtr + 0x14A) = 0;
// this just probably needs to be set
agent->AddonId = (uint) addon->ID;
// vcall from E8 ?? ?? ?? ?? 0F B7 C0 48 83 C4 60
var vf5 = (delegate*<AtkUnitBase*, byte, uint, void>*) ((IntPtr) addon->VTable + 40);
// E8872D: lets vf5 actually run
*(byte*) ((IntPtr) atkStage + 0x2B4) |= 2;
(*vf5)(addon, 0, 15);
}
internal static void CloseItemTooltip() {
// hide addon first to prevent the "addon close" sound
var addon = AtkStage.GetSingleton()->RaptureAtkUnitManager->GetAddonByName("ItemDetail");
if (addon != null) {
addon->Hide(true);
}
var agent = Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.ItemDetail);
if (agent != null) {
agent->Hide();
}
}
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.ChatActivated?.Invoke(eventInput);
} catch (Exception ex) {
PluginLog.LogError(ex, "Error in ChatActivated event");
}
return 0;
}
return this.ChatLogRefreshHook!.Original(log, eventId, value);
}
private IntPtr ChangeChannelNameDetour(IntPtr agent) {
// Last ShB patch
// +0x40 = chat channel (byte or uint?)
// channel is 17 (maybe 18?) for tells
// +0x48 = pointer to channel name string
var ret = this.ChangeChannelNameHook!.Original(agent);
if (agent == IntPtr.Zero) {
return ret;
}
// E8 ?? ?? ?? ?? 8D 48 F7
// RaptureShellModule + 0xFD0
var shellModule = (IntPtr) Framework.Instance()->GetUiModule()->GetRaptureShellModule();
if (shellModule == IntPtr.Zero) {
return ret;
}
var channel = *(uint*) (shellModule + 0xFD0);
// var channel = *(uint*) (agent + 0x40);
if (channel is 17 or 18) {
channel = 0;
}
SeString? name = null;
var namePtrPtr = (byte**) (agent + 0x48);
if (namePtrPtr != null) {
var namePtr = *namePtrPtr;
name = MemoryHelper.ReadSeStringNullTerminated((IntPtr) namePtr);
if (name.Payloads.Count == 0) {
name = null;
}
}
if (name == null) {
return ret;
}
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;
}
// These context menu things come from AgentChatLog.vf0 at the bottom
// 0x10000: item comparison
// 0x10001: try on
// 0x10002: search for item
// 0x10003: link
// 0x10005: copy item name
// 0x10006: search recipes using this material
internal void TryOn(uint itemId, byte stainId) {
if (this._tryOn == null) {
return;
}
this._tryOn(0xFF, itemId, stainId, 0, 0);
}
internal void LinkItem(uint itemId) {
if (this._linkItem == null) {
return;
}
var agent = Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.ChatLog);
this._linkItem(agent, itemId);
}
internal void OpenItemComparison(uint itemId) {
if (this._itemComparison == null) {
return;
}
var agent = Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.ItemCompare);
this._itemComparison(agent, 0x4D, itemId, 0);
}
internal void SearchForRecipesUsingItem(uint itemId) {
if (this._searchForRecipesUsingItem == null) {
return;
}
var uiModule = Framework.Instance()->GetUiModule();
var vf35 = (delegate* unmanaged<UIModule*, IntPtr>) uiModule->vfunc[35];
var a1 = vf35(uiModule);
this._searchForRecipesUsingItem(a1, itemId);
}
internal void SearchForItem(uint itemId) {
if (this._searchForItem == null) {
return;
}
var itemFinder = Framework.Instance()->GetUiModule()->GetItemFinderModule();
this._searchForItem(itemFinder, itemId, 1);
}
internal static void OpenPartyFinder() {
// this whole method: 6.05: 84433A
var lfg = Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.LookingForGroup);
if (lfg->IsAgentActive()) {
var addonId = lfg->GetAddonID();
var atkModule = Framework.Instance()->GetUiModule()->GetRaptureAtkModule();
var atkModuleVtbl = (void**) atkModule->AtkModule.vtbl;
var vf27 = (delegate* unmanaged<RaptureAtkModule*, ulong, ulong, byte>) atkModuleVtbl[27];
vf27(atkModule, addonId, 1);
} else {
// 6.05: 8443DD
if (*(uint*) ((IntPtr) lfg + 0x2AB8) > 0) {
lfg->Hide();
} else {
lfg->Show();
}
}
}
internal bool IsMentor() {
if (this._isMentor == null || this._isMentorA1 == null || this._isMentorA1.Value == IntPtr.Zero) {
return false;
}
return this._isMentor(this._isMentorA1.Value) > 0;
}
internal void KickFromParty(string name, ulong contentId) {
if (this._kick == null) {
return;
}
var agent = Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.SocialPartyMember);
if (agent == null) {
return;
}
fixed (byte* namePtr = name.ToTerminatedBytes()) {
this._kick(agent, namePtr, 0, contentId);
}
}
internal void Promote(string name, ulong contentId) {
if (this._promote == null) {
return;
}
var agent = Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.SocialPartyMember);
if (agent == null) {
return;
}
fixed (byte* namePtr = name.ToTerminatedBytes()) {
this._promote(agent, namePtr, 0, contentId);
}
}
private readonly IntPtr _placeholderNamePtr = Marshal.AllocHGlobal(128);
private readonly string _placeholder = $"<{Guid.NewGuid():N}>";
private string? _replacementName;
private IntPtr ResolveTextCommandPlaceholderDetour(IntPtr a1, byte* placeholderText, byte a3, byte a4) {
if (this._replacementName == null) {
goto Original;
}
var placeholder = MemoryHelper.ReadStringNullTerminated((IntPtr) placeholderText);
if (placeholder != this._placeholder) {
goto Original;
}
MemoryHelper.WriteString(this._placeholderNamePtr, this._replacementName);
this._replacementName = null;
return this._placeholderNamePtr;
Original:
return this.ResolveTextCommandPlaceholderHook!.Original(a1, placeholderText, a3, a4);
}
}

View File

@ -1,219 +0,0 @@
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.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)
)]
private Hook<ChatLogRefreshDelegate>? ChatLogRefreshHook { get; init; }
[Signature(
"E8 ?? ?? ?? ?? BA ?? ?? ?? ?? 48 8D 4D B0 48 8B F8 E8 ?? ?? ?? ?? 41 8B D6",
DetourName = nameof(ChangeChannelNameDetour)
)]
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;
private Plugin Plugin { get; }
internal (InputChannel channel, List<Chunk> name) Channel { get; private set; }
internal Chat(Plugin plugin) {
this.Plugin = plugin;
Siggingway.Siggingway.Initialise(this.Plugin.SigScanner, this);
this.ChatLogRefreshHook?.Enable();
this.ChangeChannelNameHook?.Enable();
this.ReplyInSelectedChatModeHook?.Enable();
this.Plugin.ClientState.Login += this.Login;
this.Login(null, null);
}
public void Dispose() {
this.Plugin.ClientState.Login -= this.Login;
this.ReplyInSelectedChatModeHook?.Dispose();
this.ChangeChannelNameHook?.Dispose();
this.ChatLogRefreshHook?.Dispose();
this.Activated = null;
}
private void Login(object? sender, EventArgs? e) {
if (this.ChangeChannelNameHook == null) {
return;
}
var agent = Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.ChatLog);
if (agent == null) {
return;
}
this.ChangeChannelNameDetour((IntPtr) agent);
}
private IntPtr ChangeChannelNameDetour(IntPtr agent) {
// Last ShB patch
// +0x40 = chat channel (byte or uint?)
// channel is 17 (maybe 18?) for tells
// +0x48 = pointer to channel name string
var ret = this.ChangeChannelNameHook!.Original(agent);
if (agent == IntPtr.Zero) {
return ret;
}
// E8 ?? ?? ?? ?? 8D 48 F7
// RaptureShellModule + 0xFD0
var shellModule = (IntPtr) Framework.Instance()->GetUiModule()->GetRaptureShellModule();
if (shellModule == IntPtr.Zero) {
return ret;
}
var channel = *(uint*) (shellModule + 0xFD0);
// var channel = *(uint*) (agent + 0x40);
if (channel is 17 or 18) {
channel = 0;
}
SeString? name = null;
var namePtrPtr = (byte**) (agent + 0x48);
if (namePtrPtr != null) {
var namePtr = *namePtrPtr;
name = MemoryHelper.ReadSeStringNullTerminated((IntPtr) namePtr);
if (name.Payloads.Count == 0) {
name = null;
}
}
if (name == null) {
return ret;
}
var nameChunks = ChunkUtil.ToChunks(name, null).ToList();
if (nameChunks.Count > 0 && nameChunks[0] is TextChunk text) {
text.Content = text.Content.TrimStart('\uE01E').TrimStart();
}
this.Channel = ((InputChannel) channel, nameChunks);
return ret;
}
private byte ChatLogRefreshDetour(IntPtr log, ushort eventId, AtkValue* value) {
if (eventId != 0x31 || value == null || value->UInt is not (0x05 or 0x0C)) {
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) {
if (this._getContentIdForChatEntry == null) {
return null;
}
return this._getContentIdForChatEntry(Framework.Instance()->GetUiModule()->GetRaptureLogModule(), index);
}
internal void SetChannel(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);
}
}
}

View File

@ -1,107 +0,0 @@
using ChatTwo.Util;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Siggingway;
namespace ChatTwo.GameFunctions;
internal sealed unsafe class Context {
[Signature("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 48 8B CB E8 ?? ?? ?? ?? 45 33 C9", Fallibility = Fallibility.Fallible)]
private readonly delegate* unmanaged<IntPtr, ulong, ushort, byte*, byte> _inviteToNoviceNetwork = null!;
[Signature("E8 ?? ?? ?? ?? EB 35 BA", Fallibility = Fallibility.Fallible)]
private readonly delegate* unmanaged<uint, uint, ulong, uint, byte, byte> _tryOn = null!;
[Signature("E8 ?? ?? ?? ?? EB 7B 49 8B 06", Fallibility = Fallibility.Fallible)]
private readonly delegate* unmanaged<AgentInterface*, uint, void> _linkItem = null!;
[Signature("E8 ?? ?? ?? ?? EB 3F 83 F8 FE", Fallibility = Fallibility.Fallible)]
private readonly delegate* unmanaged<AgentInterface*, ushort, uint, byte, void> _itemComparison = null!;
[Signature("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 41 B4 01", Fallibility = Fallibility.Fallible)]
private readonly delegate* unmanaged<IntPtr, uint, void> _searchForRecipesUsingItem = null!;
[Signature("E8 ?? ?? ?? ?? EB 45 45 33 C9", Fallibility = Fallibility.Fallible)]
private readonly delegate* unmanaged<void*, uint, byte, void> _searchForItem = null!;
private Plugin Plugin { get; }
internal Context(Plugin plugin) {
this.Plugin = plugin;
Siggingway.Siggingway.Initialise(this.Plugin.SigScanner, this);
}
internal void InviteToNoviceNetwork(string name, ushort world) {
if (this._inviteToNoviceNetwork == null || this.Plugin.Functions.Indexer == null) {
return;
}
var uiModule = Framework.Instance()->GetUiModule();
// 6.05: 20D722
var func = (delegate*<UIModule*, IntPtr>) uiModule->vfunc[33];
var toIndex = func(uiModule);
// 6.05: 20E4CB
var a1 = this.Plugin.Functions.Indexer(toIndex, 0x11);
fixed (byte* namePtr = name.ToTerminatedBytes()) {
// can specify content id if we have it, but there's no need
this._inviteToNoviceNetwork(a1, 0, world, namePtr);
}
}
// These context menu things come from AgentChatLog.vf0 at the bottom
// 0x10000: item comparison
// 0x10001: try on
// 0x10002: search for item
// 0x10003: link
// 0x10005: copy item name
// 0x10006: search recipes using this material
internal void TryOn(uint itemId, byte stainId) {
if (this._tryOn == null) {
return;
}
this._tryOn(0xFF, itemId, stainId, 0, 0);
}
internal void LinkItem(uint itemId) {
if (this._linkItem == null) {
return;
}
var agent = Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.ChatLog);
this._linkItem(agent, itemId);
}
internal void OpenItemComparison(uint itemId) {
if (this._itemComparison == null) {
return;
}
var agent = Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.ItemCompare);
this._itemComparison(agent, 0x4D, itemId, 0);
}
internal void SearchForRecipesUsingItem(uint itemId) {
if (this._searchForRecipesUsingItem == null) {
return;
}
var uiModule = Framework.Instance()->GetUiModule();
var vf35 = (delegate* unmanaged<UIModule*, IntPtr>) uiModule->vfunc[35];
var a1 = vf35(uiModule);
this._searchForRecipesUsingItem(a1, itemId);
}
internal void SearchForItem(uint itemId) {
if (this._searchForItem == null) {
return;
}
var itemFinder = Framework.Instance()->GetUiModule()->GetItemFinderModule();
this._searchForItem(itemFinder, itemId, 1);
}
}

View File

@ -1,234 +0,0 @@
using System.Runtime.InteropServices;
using Dalamud.Hooking;
using Dalamud.Memory;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Lumina.Excel.GeneratedSheets;
using Siggingway;
namespace ChatTwo.GameFunctions;
internal unsafe class GameFunctions : IDisposable {
private static class Signatures {
internal const string IsMentorA1 = "48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 84 C0 74 71 0F B6 86";
internal const string ResolveTextCommandPlaceholder = "E8 ?? ?? ?? ?? 49 8D 4F 18 4C 8B E0";
internal const string CurrentChatEntryOffset = "8B 77 ?? 8D 46 01 89 47 14 81 FE ?? ?? ?? ?? 72 03 FF 47";
}
#region Functions
[Signature("E8 ?? ?? ?? ?? 8B FD 8B CD", Fallibility = Fallibility.Fallible)]
internal readonly delegate* unmanaged<IntPtr, uint, IntPtr> Indexer = null!;
[Signature("E8 ?? ?? ?? ?? 84 C0 74 0D B0 02", Fallibility = Fallibility.Fallible)]
private readonly delegate* unmanaged<IntPtr, byte> _isMentor = null!;
#endregion
#region Hooks
private delegate IntPtr ResolveTextCommandPlaceholderDelegate(IntPtr a1, byte* placeholderText, byte a3, byte a4);
[Signature(Signatures.ResolveTextCommandPlaceholder, DetourName = nameof(ResolveTextCommandPlaceholderDetour))]
private Hook<ResolveTextCommandPlaceholderDelegate>? ResolveTextCommandPlaceholderHook { get; init; }
#endregion
#pragma warning disable 0649
[Signature(Signatures.CurrentChatEntryOffset, Offset = 2)]
private readonly byte? _currentChatEntryOffset;
[Signature(Signatures.IsMentorA1, ScanType = ScanType.StaticAddress)]
private readonly IntPtr? _isMentorA1;
#pragma warning restore 0649
internal const int HqItemOffset = 1_000_000;
private Plugin Plugin { get; }
internal Party Party { get; }
internal Chat Chat { get; }
internal Context Context { get; }
internal GameFunctions(Plugin plugin) {
this.Plugin = plugin;
this.Party = new Party(this.Plugin);
this.Chat = new Chat(this.Plugin);
this.Context = new Context(this.Plugin);
Siggingway.Siggingway.Initialise(this.Plugin.SigScanner, this);
this.ResolveTextCommandPlaceholderHook?.Enable();
}
public void Dispose() {
this.Chat.Dispose();
this.ResolveTextCommandPlaceholderHook?.Dispose();
Marshal.FreeHGlobal(this._placeholderNamePtr);
}
internal uint? GetCurrentChatLogEntryIndex() {
if (this._currentChatEntryOffset == null) {
return null;
}
var log = (IntPtr) Framework.Instance()->GetUiModule()->GetRaptureLogModule();
return *(uint*) (log + this._currentChatEntryOffset.Value);
}
internal void SendFriendRequest(string name, ushort world) {
var row = this.Plugin.DataManager.GetExcelSheet<World>()!.GetRow(world);
if (row == null) {
return;
}
var worldName = row.Name.RawString;
this._replacementName = $"{name}@{worldName}";
this.Plugin.Common.Functions.Chat.SendMessage($"/friendlist add {this._placeholder}");
}
internal static void SetAddonInteractable(string name, bool interactable) {
var unitManager = AtkStage.GetSingleton()->RaptureAtkUnitManager;
var addon = (IntPtr) unitManager->GetAddonByName(name);
if (addon == IntPtr.Zero) {
return;
}
var flags = (uint*) (addon + 0x180);
if (interactable) {
*flags &= ~(1u << 22);
} else {
*flags |= 1 << 22;
}
}
internal static void SetChatInteractable(bool interactable) {
for (var i = 0; i < 4; i++) {
SetAddonInteractable($"ChatLogPanel_{i}", interactable);
}
SetAddonInteractable("ChatLog", interactable);
}
internal static bool IsAddonInteractable(string name) {
var unitManager = AtkStage.GetSingleton()->RaptureAtkUnitManager;
var addon = (IntPtr) unitManager->GetAddonByName(name);
if (addon == IntPtr.Zero) {
return false;
}
var flags = (uint*) (addon + 0x180);
return (*flags & (1 << 22)) == 0;
}
internal static void OpenItemTooltip(uint id) {
var atkStage = AtkStage.GetSingleton();
var agent = Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.ItemDetail);
var addon = atkStage->RaptureAtkUnitManager->GetAddonByName("ItemDetail");
if (agent == null || addon == null) {
// atkStage ain't gonna be null or we have bigger problems
return;
}
var agentPtr = (IntPtr) agent;
// addresses mentioned here are 6.01
// see the call near the end of AgentItemDetail.Update
// offsets valid as of 6.01
// 8BFC49: sets some shit
*(uint*) (agentPtr + 0x20) = 22;
// 8C04C8: switch goes down to default, which is what we want
*(byte*) (agentPtr + 0x118) = 1;
// 8BFCF6: item id when hovering over item in chat
*(uint*) (agentPtr + 0x11C) = id;
// 8BFCE4: always 0 when hovering over item in chat
*(uint*) (agentPtr + 0x120) = 0;
// 8C0B55: skips a check to do with inventory
*(byte*) (agentPtr + 0x128) &= 0xEF;
// 8BFC7C: when set to 1, lets everything continue (one frame)
*(byte*) (agentPtr + 0x146) = 1;
// 8BFC89: skips early return
*(byte*) (agentPtr + 0x14A) = 0;
// this just probably needs to be set
agent->AddonId = (uint) addon->ID;
// vcall from E8 ?? ?? ?? ?? 0F B7 C0 48 83 C4 60
var vf5 = (delegate*<AtkUnitBase*, byte, uint, void>*) ((IntPtr) addon->VTable + 40);
// E8872D: lets vf5 actually run
*(byte*) ((IntPtr) atkStage + 0x2B4) |= 2;
(*vf5)(addon, 0, 15);
}
internal static void CloseItemTooltip() {
// hide addon first to prevent the "addon close" sound
var addon = AtkStage.GetSingleton()->RaptureAtkUnitManager->GetAddonByName("ItemDetail");
if (addon != null) {
addon->Hide(true);
}
var agent = Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.ItemDetail);
if (agent != null) {
agent->Hide();
}
}
internal static void OpenPartyFinder() {
// this whole method: 6.05: 84433A
var lfg = Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.LookingForGroup);
if (lfg->IsAgentActive()) {
var addonId = lfg->GetAddonID();
var atkModule = Framework.Instance()->GetUiModule()->GetRaptureAtkModule();
var atkModuleVtbl = (void**) atkModule->AtkModule.vtbl;
var vf27 = (delegate* unmanaged<RaptureAtkModule*, ulong, ulong, byte>) atkModuleVtbl[27];
vf27(atkModule, addonId, 1);
} else {
// 6.05: 8443DD
if (*(uint*) ((IntPtr) lfg + 0x2AB8) > 0) {
lfg->Hide();
} else {
lfg->Show();
}
}
}
internal bool IsMentor() {
if (this._isMentor == null || this._isMentorA1 == null || this._isMentorA1.Value == IntPtr.Zero) {
return false;
}
return this._isMentor(this._isMentorA1.Value) > 0;
}
private readonly IntPtr _placeholderNamePtr = Marshal.AllocHGlobal(128);
private readonly string _placeholder = $"<{Guid.NewGuid():N}>";
private string? _replacementName;
private IntPtr ResolveTextCommandPlaceholderDetour(IntPtr a1, byte* placeholderText, byte a3, byte a4) {
if (this._replacementName == null) {
goto Original;
}
var placeholder = MemoryHelper.ReadStringNullTerminated((IntPtr) placeholderText);
if (placeholder != this._placeholder) {
goto Original;
}
MemoryHelper.WriteString(this._placeholderNamePtr, this._replacementName);
this._replacementName = null;
return this._placeholderNamePtr;
Original:
return this.ResolveTextCommandPlaceholderHook!.Original(a1, placeholderText, a3, a4);
}
}

View File

@ -1,73 +0,0 @@
using ChatTwo.Util;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Siggingway;
namespace ChatTwo.GameFunctions;
internal sealed unsafe class Party {
[Signature("E8 ?? ?? ?? ?? 33 C0 EB 51", Fallibility = Fallibility.Fallible)]
private readonly delegate* unmanaged<IntPtr, ulong, byte*, ushort, void> _inviteToParty = null!;
[Signature("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 49 8B 56 20", Fallibility = Fallibility.Fallible)]
private readonly delegate* unmanaged<AgentInterface*, byte*, ushort, ulong, void> _promote = null!;
[Signature("E8 ?? ?? ?? ?? EB 66 49 8B 4E 20", Fallibility = Fallibility.Fallible)]
private readonly delegate* unmanaged<AgentInterface*, byte*, ushort, ulong, void> _kick = null!;
private Plugin Plugin { get; }
internal Party(Plugin plugin) {
this.Plugin = plugin;
Siggingway.Siggingway.Initialise(this.Plugin.SigScanner, this);
}
internal void Invite(string name, ushort world) {
if (this._inviteToParty == null || this.Plugin.Functions.Indexer == null) {
return;
}
var uiModule = Framework.Instance()->GetUiModule();
// 6.05: 20D722
var func = (delegate*<UIModule*, IntPtr>) uiModule->vfunc[33];
var toIndex = func(uiModule);
var a1 = this.Plugin.Functions.Indexer(toIndex, 1);
fixed (byte* namePtr = name.ToTerminatedBytes()) {
// can specify content id if we have it, but there's no need
this._inviteToParty(a1, 0, namePtr, world);
}
}
internal void Kick(string name, ulong contentId) {
if (this._kick == null) {
return;
}
var agent = Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.SocialPartyMember);
if (agent == null) {
return;
}
fixed (byte* namePtr = name.ToTerminatedBytes()) {
this._kick(agent, namePtr, 0, contentId);
}
}
internal void Promote(string name, ulong contentId) {
if (this._promote == null) {
return;
}
var agent = Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.SocialPartyMember);
if (agent == null) {
return;
}
fixed (byte* namePtr = name.ToTerminatedBytes()) {
this._promote(agent, namePtr, 0, contentId);
}
}
}

View File

@ -15,9 +15,5 @@ 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,6 +1,5 @@
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;
@ -17,7 +16,7 @@ internal sealed class PayloadHandler {
private PluginUi Ui { get; } private PluginUi Ui { get; }
private ChatLog Log { get; } private ChatLog Log { get; }
private HashSet<(Chunk, Payload)> PopupPayloads { get; set; } = new(); private HashSet<Payload> PopupPayloads { get; set; } = new();
private bool _handleTooltips; private bool _handleTooltips;
private uint _hoveredItem; private uint _hoveredItem;
@ -33,7 +32,7 @@ internal sealed class PayloadHandler {
this.DrawPopups(); this.DrawPopups();
if (this._handleTooltips && ++this._hoverCounter - this._lastHoverCounter > 1) { if (this._handleTooltips && ++this._hoverCounter - this._lastHoverCounter > 1) {
GameFunctions.GameFunctions.CloseItemTooltip(); GameFunctions.CloseItemTooltip();
this._hoveredItem = 0; this._hoveredItem = 0;
this._hoverCounter = this._lastHoverCounter = 0; this._hoverCounter = this._lastHoverCounter = 0;
this._handleTooltips = false; this._handleTooltips = false;
@ -41,8 +40,8 @@ internal sealed class PayloadHandler {
} }
private void DrawPopups() { private void DrawPopups() {
var newPopups = new HashSet<(Chunk, Payload)>(); var newPopups = new HashSet<Payload>();
foreach (var (chunk, payload) in this.PopupPayloads) { foreach (var payload in this.PopupPayloads) {
var id = PopupId(payload); var id = PopupId(payload);
if (id == null) { if (id == null) {
continue; continue;
@ -52,12 +51,12 @@ internal sealed class PayloadHandler {
continue; continue;
} }
newPopups.Add((chunk, payload)); newPopups.Add(payload);
ImGui.PushID(id); ImGui.PushID(id);
switch (payload) { switch (payload) {
case PlayerPayload player: { case PlayerPayload player: {
this.DrawPlayerPopup(chunk, player); this.DrawPlayerPopup(player);
break; break;
} }
case ItemPayload item: { case ItemPayload item: {
@ -79,7 +78,7 @@ internal sealed class PayloadHandler {
this.LeftClickPayload(chunk, payload); this.LeftClickPayload(chunk, payload);
break; break;
case ImGuiMouseButton.Right: case ImGuiMouseButton.Right:
this.RightClickPayload(chunk, payload); this.RightClickPayload(payload);
break; break;
} }
} }
@ -92,7 +91,7 @@ internal sealed class PayloadHandler {
} }
case ItemPayload item: { case ItemPayload item: {
if (this.Ui.Plugin.Config.NativeItemTooltips) { if (this.Ui.Plugin.Config.NativeItemTooltips) {
GameFunctions.GameFunctions.OpenItemTooltip(item.ItemId); GameFunctions.OpenItemTooltip(item.ItemId);
this._handleTooltips = true; this._handleTooltips = true;
if (this._hoveredItem != item.ItemId) { if (this._hoveredItem != item.ItemId) {
@ -178,7 +177,7 @@ internal sealed class PayloadHandler {
} }
case RawPayload raw: { case RawPayload raw: {
if (Equals(raw, ChunkUtil.PeriodicRecruitmentLink)) { if (Equals(raw, ChunkUtil.PeriodicRecruitmentLink)) {
GameFunctions.GameFunctions.OpenPartyFinder(); GameFunctions.OpenPartyFinder();
} }
break; break;
@ -212,11 +211,11 @@ internal sealed class PayloadHandler {
} }
} }
private void RightClickPayload(Chunk chunk, Payload payload) { private void RightClickPayload(Payload payload) {
switch (payload) { switch (payload) {
case PlayerPayload: case PlayerPayload:
case ItemPayload: { case ItemPayload: {
this.PopupPayloads.Add((chunk, payload)); this.PopupPayloads.Add(payload);
ImGui.OpenPopup(PopupId(payload)); ImGui.OpenPopup(PopupId(payload));
break; break;
} }
@ -237,30 +236,30 @@ internal sealed class PayloadHandler {
this.Log.DrawChunks(ChunkUtil.ToChunks(name, null).ToList(), false); this.Log.DrawChunks(ChunkUtil.ToChunks(name, null).ToList(), false);
ImGui.Separator(); ImGui.Separator();
var realItemId = (uint) (item.ItemId + (item.IsHQ ? GameFunctions.GameFunctions.HqItemOffset : 0)); var realItemId = (uint) (item.ItemId + (item.IsHQ ? GameFunctions.HqItemOffset : 0));
if (item.Item.EquipSlotCategory.Row != 0) { if (item.Item.EquipSlotCategory.Row != 0) {
if (ImGui.Selectable("Try On")) { if (ImGui.Selectable("Try On")) {
this.Ui.Plugin.Functions.Context.TryOn(realItemId, 0); this.Ui.Plugin.Functions.TryOn(realItemId, 0);
} }
if (ImGui.Selectable("Item Comparison")) { if (ImGui.Selectable("Item Comparison")) {
this.Ui.Plugin.Functions.Context.OpenItemComparison(realItemId); this.Ui.Plugin.Functions.OpenItemComparison(realItemId);
} }
} }
if (item.Item.ItemSearchCategory.Value?.Category == 3) { if (item.Item.ItemSearchCategory.Value?.Category == 3) {
if (ImGui.Selectable("Search Recipes Using This Material")) { if (ImGui.Selectable("Search Recipes Using This Material")) {
this.Ui.Plugin.Functions.Context.SearchForRecipesUsingItem(item.ItemId); this.Ui.Plugin.Functions.SearchForRecipesUsingItem(item.ItemId);
} }
} }
if (ImGui.Selectable("Search for Item")) { if (ImGui.Selectable("Search for Item")) {
this.Ui.Plugin.Functions.Context.SearchForItem(realItemId); this.Ui.Plugin.Functions.SearchForItem(realItemId);
} }
if (ImGui.Selectable("Link")) { if (ImGui.Selectable("Link")) {
this.Ui.Plugin.Functions.Context.LinkItem(realItemId); this.Ui.Plugin.Functions.LinkItem(realItemId);
} }
if (ImGui.Selectable("Copy Item Name")) { if (ImGui.Selectable("Copy Item Name")) {
@ -268,7 +267,7 @@ internal sealed class PayloadHandler {
} }
} }
private void DrawPlayerPopup(Chunk chunk, PlayerPayload player) { private void DrawPlayerPopup(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}";
@ -290,16 +289,16 @@ internal sealed class PayloadHandler {
var isInParty = member != default; var isInParty = member != default;
if (isLeader) { if (isLeader) {
if (!isInParty && ImGui.Selectable("Invite to Party")) { if (!isInParty && ImGui.Selectable("Invite to Party")) {
this.Ui.Plugin.Functions.Party.Invite(player.PlayerName, (ushort) player.World.RowId); this.Ui.Plugin.Functions.InviteToParty(player.PlayerName, (ushort) player.World.RowId);
} }
if (isInParty && member != null) { if (isInParty && member != null) {
if (ImGui.Selectable("Promote")) { if (ImGui.Selectable("Promote")) {
this.Ui.Plugin.Functions.Party.Promote(player.PlayerName, (ulong) member.ContentId); this.Ui.Plugin.Functions.Promote(player.PlayerName, (ulong) member.ContentId);
} }
if (ImGui.Selectable("Kick from Party")) { if (ImGui.Selectable("Kick from Party")) {
this.Ui.Plugin.Functions.Party.Kick(player.PlayerName, (ulong) member.ContentId); this.Ui.Plugin.Functions.KickFromParty(player.PlayerName, (ulong) member.ContentId);
} }
} }
} }
@ -310,16 +309,10 @@ internal sealed class PayloadHandler {
} }
if (this.Ui.Plugin.Functions.IsMentor() && ImGui.Selectable("Invite to Novice Network")) { if (this.Ui.Plugin.Functions.IsMentor() && ImGui.Selectable("Invite to Novice Network")) {
this.Ui.Plugin.Functions.Context.InviteToNoviceNetwork(player.PlayerName, (ushort) player.World.RowId); this.Ui.Plugin.Functions.InviteToNoviceNetwork(player.PlayerName, (ushort) player.World.RowId);
} }
} }
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);
} }
@ -330,6 +323,7 @@ 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

@ -51,7 +51,7 @@ public sealed class Plugin : IDalamudPlugin {
internal Configuration Config { get; } internal Configuration Config { get; }
internal XivCommonBase Common { get; } internal XivCommonBase Common { get; }
internal TextureCache TextureCache { get; } internal TextureCache TextureCache { get; }
internal GameFunctions.GameFunctions Functions { get; } internal GameFunctions Functions { get; }
internal Store Store { get; } internal Store Store { get; }
internal PluginUi Ui { get; } internal PluginUi Ui { get; }
@ -60,7 +60,7 @@ public sealed class Plugin : IDalamudPlugin {
this.Config = this.Interface!.GetPluginConfig() as Configuration ?? new Configuration(); this.Config = this.Interface!.GetPluginConfig() as Configuration ?? new Configuration();
this.Common = new XivCommonBase(); this.Common = new XivCommonBase();
this.TextureCache = new TextureCache(this.DataManager!); this.TextureCache = new TextureCache(this.DataManager!);
this.Functions = new GameFunctions.GameFunctions(this); this.Functions = new GameFunctions(this);
this.Store = new Store(this); this.Store = new Store(this);
this.Ui = new PluginUi(this); this.Ui = new PluginUi(this);
@ -70,7 +70,7 @@ public sealed class Plugin : IDalamudPlugin {
public void Dispose() { public void Dispose() {
this.Framework.Update -= this.FrameworkUpdate; this.Framework.Update -= this.FrameworkUpdate;
GameFunctions.GameFunctions.SetChatInteractable(true); GameFunctions.SetChatInteractable(true);
this.Ui.Dispose(); this.Ui.Dispose();
this.Store.Dispose(); this.Store.Dispose();
@ -97,8 +97,8 @@ public sealed class Plugin : IDalamudPlugin {
} }
foreach (var name in ChatAddonNames) { foreach (var name in ChatAddonNames) {
if (GameFunctions.GameFunctions.IsAddonInteractable(name)) { if (GameFunctions.IsAddonInteractable(name)) {
GameFunctions.GameFunctions.SetAddonInteractable(name, false); GameFunctions.SetAddonInteractable(name, false);
} }
} }
} }

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 readonly ImVector _ranges; private ImVector _ranges;
private GCHandle _jpRange = GCHandle.Alloc( private GCHandle _jpRange = GCHandle.Alloc(
GlyphRangesJapanese.GlyphRanges, GlyphRangesJapanese.GlyphRanges,

View File

@ -52,7 +52,7 @@ internal class Store : IDisposable {
return; return;
} }
var contentId = this.Plugin.Functions.Chat.GetContentIdForEntry(entry.Item1); var contentId = this.Plugin.Functions.GetContentIdForChatLogEntry(entry.Item1);
entry.Item2.ContentId = contentId ?? 0; entry.Item2.ContentId = contentId ?? 0;
} }

View File

@ -29,15 +29,15 @@ internal sealed class ChatLog : IUiComponent {
this._fontIcon = this.Ui.Plugin.DataManager.GetImGuiTexture("common/font/fonticon_ps5.tex"); this._fontIcon = this.Ui.Plugin.DataManager.GetImGuiTexture("common/font/fonticon_ps5.tex");
this.Ui.Plugin.Functions.Chat.Activated += this.Activated; this.Ui.Plugin.Functions.ChatActivated += this.ChatActivated;
} }
public void Dispose() { public void Dispose() {
this.Ui.Plugin.Functions.Chat.Activated -= this.Activated; this.Ui.Plugin.Functions.ChatActivated -= this.ChatActivated;
this._fontIcon?.Dispose(); this._fontIcon?.Dispose();
} }
private void Activated(string? input) { private void ChatActivated(string? input) {
this.Activate = true; this.Activate = true;
if (input != null && !this.Chat.Contains(input)) { if (input != null && !this.Chat.Contains(input)) {
this.Chat += input; this.Chat += input;
@ -88,7 +88,7 @@ internal sealed class ChatLog : IUiComponent {
if (activeTab is { Channel: { } channel }) { if (activeTab is { Channel: { } channel }) {
ImGui.TextUnformatted(channel.ToChatType().Name()); ImGui.TextUnformatted(channel.ToChatType().Name());
} else { } else {
this.DrawChunks(this.Ui.Plugin.Functions.Chat.Channel.name); this.DrawChunks(this.Ui.Plugin.Functions.ChatChannel.name);
} }
} finally { } finally {
ImGui.PopStyleVar(); ImGui.PopStyleVar();
@ -114,7 +114,7 @@ internal sealed class ChatLog : IUiComponent {
?.RawString ?? channel.ToString(); ?.RawString ?? channel.ToString();
if (ImGui.Selectable(name)) { if (ImGui.Selectable(name)) {
this.Ui.Plugin.Functions.Chat.SetChannel(channel); this.Ui.Plugin.Functions.SetChatChannel(channel);
} }
} }
@ -127,7 +127,7 @@ internal sealed class ChatLog : IUiComponent {
var buttonWidth = afterIcon.X - beforeIcon.X; var buttonWidth = afterIcon.X - beforeIcon.X;
var inputWidth = ImGui.GetContentRegionAvail().X - buttonWidth; var inputWidth = ImGui.GetContentRegionAvail().X - buttonWidth;
var inputType = this.Ui.Plugin.Functions.Chat.Channel.channel.ToChatType(); var inputType = this.Ui.Plugin.Functions.ChatChannel.channel.ToChatType();
var inputColour = this.Ui.Plugin.Config.ChatColours.TryGetValue(inputType, out var inputCol) var inputColour = this.Ui.Plugin.Config.ChatColours.TryGetValue(inputType, out var inputCol)
? inputCol ? inputCol
: inputType.DefaultColour(); : inputType.DefaultColour();

View File

@ -197,7 +197,7 @@ internal sealed class Settings : IUiComponent {
} }
if (!this._hideChat && hideChatChanged) { if (!this._hideChat && hideChatChanged) {
GameFunctions.GameFunctions.SetChatInteractable(true); GameFunctions.SetChatInteractable(true);
} }
this.Initialise(); this.Initialise();