Compare commits

...

2 Commits

14 changed files with 711 additions and 561 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

@ -1,523 +0,0 @@
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);
}
}

219
ChatTwo/GameFunctions/Chat.cs Executable file
View File

@ -0,0 +1,219 @@
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);
}
}
}

107
ChatTwo/GameFunctions/Context.cs Executable file
View File

@ -0,0 +1,107 @@
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

@ -0,0 +1,234 @@
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);
}
}

73
ChatTwo/GameFunctions/Party.cs Executable file
View File

@ -0,0 +1,73 @@
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,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;
@ -32,7 +33,7 @@ internal sealed class PayloadHandler {
this.DrawPopups();
if (this._handleTooltips && ++this._hoverCounter - this._lastHoverCounter > 1) {
GameFunctions.CloseItemTooltip();
GameFunctions.GameFunctions.CloseItemTooltip();
this._hoveredItem = 0;
this._hoverCounter = this._lastHoverCounter = 0;
this._handleTooltips = false;
@ -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;
}
}
@ -91,7 +92,7 @@ internal sealed class PayloadHandler {
}
case ItemPayload item: {
if (this.Ui.Plugin.Config.NativeItemTooltips) {
GameFunctions.OpenItemTooltip(item.ItemId);
GameFunctions.GameFunctions.OpenItemTooltip(item.ItemId);
this._handleTooltips = true;
if (this._hoveredItem != item.ItemId) {
@ -177,7 +178,7 @@ internal sealed class PayloadHandler {
}
case RawPayload raw: {
if (Equals(raw, ChunkUtil.PeriodicRecruitmentLink)) {
GameFunctions.OpenPartyFinder();
GameFunctions.GameFunctions.OpenPartyFinder();
}
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;
}
@ -236,30 +237,30 @@ internal sealed class PayloadHandler {
this.Log.DrawChunks(ChunkUtil.ToChunks(name, null).ToList(), false);
ImGui.Separator();
var realItemId = (uint) (item.ItemId + (item.IsHQ ? GameFunctions.HqItemOffset : 0));
var realItemId = (uint) (item.ItemId + (item.IsHQ ? GameFunctions.GameFunctions.HqItemOffset : 0));
if (item.Item.EquipSlotCategory.Row != 0) {
if (ImGui.Selectable("Try On")) {
this.Ui.Plugin.Functions.TryOn(realItemId, 0);
this.Ui.Plugin.Functions.Context.TryOn(realItemId, 0);
}
if (ImGui.Selectable("Item Comparison")) {
this.Ui.Plugin.Functions.OpenItemComparison(realItemId);
this.Ui.Plugin.Functions.Context.OpenItemComparison(realItemId);
}
}
if (item.Item.ItemSearchCategory.Value?.Category == 3) {
if (ImGui.Selectable("Search Recipes Using This Material")) {
this.Ui.Plugin.Functions.SearchForRecipesUsingItem(item.ItemId);
this.Ui.Plugin.Functions.Context.SearchForRecipesUsingItem(item.ItemId);
}
}
if (ImGui.Selectable("Search for Item")) {
this.Ui.Plugin.Functions.SearchForItem(realItemId);
this.Ui.Plugin.Functions.Context.SearchForItem(realItemId);
}
if (ImGui.Selectable("Link")) {
this.Ui.Plugin.Functions.LinkItem(realItemId);
this.Ui.Plugin.Functions.Context.LinkItem(realItemId);
}
if (ImGui.Selectable("Copy Item Name")) {
@ -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}";
@ -289,16 +290,16 @@ internal sealed class PayloadHandler {
var isInParty = member != default;
if (isLeader) {
if (!isInParty && ImGui.Selectable("Invite to Party")) {
this.Ui.Plugin.Functions.InviteToParty(player.PlayerName, (ushort) player.World.RowId);
this.Ui.Plugin.Functions.Party.Invite(player.PlayerName, (ushort) player.World.RowId);
}
if (isInParty && member != null) {
if (ImGui.Selectable("Promote")) {
this.Ui.Plugin.Functions.Promote(player.PlayerName, (ulong) member.ContentId);
this.Ui.Plugin.Functions.Party.Promote(player.PlayerName, (ulong) member.ContentId);
}
if (ImGui.Selectable("Kick from Party")) {
this.Ui.Plugin.Functions.KickFromParty(player.PlayerName, (ulong) member.ContentId);
this.Ui.Plugin.Functions.Party.Kick(player.PlayerName, (ulong) member.ContentId);
}
}
}
@ -309,10 +310,16 @@ internal sealed class PayloadHandler {
}
if (this.Ui.Plugin.Functions.IsMentor() && ImGui.Selectable("Invite to Novice Network")) {
this.Ui.Plugin.Functions.InviteToNoviceNetwork(player.PlayerName, (ushort) player.World.RowId);
this.Ui.Plugin.Functions.Context.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) {
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

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

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,

View File

@ -52,7 +52,7 @@ internal class Store : IDisposable {
return;
}
var contentId = this.Plugin.Functions.GetContentIdForChatLogEntry(entry.Item1);
var contentId = this.Plugin.Functions.Chat.GetContentIdForEntry(entry.Item1);
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.Ui.Plugin.Functions.ChatActivated += this.ChatActivated;
this.Ui.Plugin.Functions.Chat.Activated += this.Activated;
}
public void Dispose() {
this.Ui.Plugin.Functions.ChatActivated -= this.ChatActivated;
this.Ui.Plugin.Functions.Chat.Activated -= this.Activated;
this._fontIcon?.Dispose();
}
private void ChatActivated(string? input) {
private void Activated(string? input) {
this.Activate = true;
if (input != null && !this.Chat.Contains(input)) {
this.Chat += input;
@ -88,7 +88,7 @@ internal sealed class ChatLog : IUiComponent {
if (activeTab is { Channel: { } channel }) {
ImGui.TextUnformatted(channel.ToChatType().Name());
} else {
this.DrawChunks(this.Ui.Plugin.Functions.ChatChannel.name);
this.DrawChunks(this.Ui.Plugin.Functions.Chat.Channel.name);
}
} finally {
ImGui.PopStyleVar();
@ -114,7 +114,7 @@ internal sealed class ChatLog : IUiComponent {
?.RawString ?? channel.ToString();
if (ImGui.Selectable(name)) {
this.Ui.Plugin.Functions.SetChatChannel(channel);
this.Ui.Plugin.Functions.Chat.SetChannel(channel);
}
}
@ -127,7 +127,7 @@ internal sealed class ChatLog : IUiComponent {
var buttonWidth = afterIcon.X - beforeIcon.X;
var inputWidth = ImGui.GetContentRegionAvail().X - buttonWidth;
var inputType = this.Ui.Plugin.Functions.ChatChannel.channel.ToChatType();
var inputType = this.Ui.Plugin.Functions.Chat.Channel.channel.ToChatType();
var inputColour = this.Ui.Plugin.Config.ChatColours.TryGetValue(inputType, out var inputCol)
? inputCol
: inputType.DefaultColour();

View File

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