using System; using System.Collections.Generic; using System.Runtime.InteropServices; using Dalamud.Game; using Dalamud.Game.Internal.Gui; using Dalamud.Game.Internal.Gui.Structs; using Dalamud.Hooking; using Dalamud.Plugin; namespace XivCommon.Functions { /// /// A class containing Party Finder functionality /// public class PartyFinder : IDisposable { private static class Signatures { internal const string RequestListings = "48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 40 0F 10 81 ?? ?? ?? ??"; internal const string JoinCrossParty = "E8 ?? ?? ?? ?? 0F B7 47 28"; } private delegate byte RequestPartyFinderListingsDelegate(IntPtr agent, byte categoryIdx); private delegate IntPtr JoinPfDelegate(IntPtr manager, IntPtr a2, int type, IntPtr packetData, uint a5); private RequestPartyFinderListingsDelegate? RequestPartyFinderListings { get; } private Hook? RequestPfListingsHook { get; } private Hook? JoinPfHook { get; } /// /// The delegate for party join events. /// public delegate void JoinPfEventDelegate(PartyFinderListing listing); /// /// /// The event that is fired when the player joins a cross-world party via Party Finder. /// /// /// Requires the hook to be enabled. /// /// public event JoinPfEventDelegate? JoinParty; private PartyFinderGui PartyFinderGui { get; } private bool JoinsEnabled { get; } private bool ListingsEnabled { get; } private IntPtr PartyFinderAgent { get; set; } = IntPtr.Zero; private Dictionary Listings { get; } = new(); private int LastBatch { get; set; } = -1; internal PartyFinder(SigScanner scanner, PartyFinderGui partyFinderGui, Hooks hooks) { this.PartyFinderGui = partyFinderGui; this.ListingsEnabled = hooks.HasFlag(Hooks.PartyFinderListings); this.JoinsEnabled = hooks.HasFlag(Hooks.PartyFinderJoins); if (scanner.TryScanText(Signatures.RequestListings, out var requestPfPtr, "Party Finder listings")) { this.RequestPartyFinderListings = Marshal.GetDelegateForFunctionPointer(requestPfPtr); if (this.ListingsEnabled) { this.RequestPfListingsHook = new Hook(requestPfPtr, new RequestPartyFinderListingsDelegate(this.OnRequestPartyFinderListings)); this.RequestPfListingsHook.Enable(); } } if (this.JoinsEnabled && scanner.TryScanText(Signatures.JoinCrossParty, out var joinPtr, "Party Finder joins")) { this.JoinPfHook = new Hook(joinPtr, new JoinPfDelegate(this.JoinPfDetour)); this.JoinPfHook.Enable(); this.PartyFinderGui.ReceiveListing += this.ReceiveListing; } } /// public void Dispose() { this.PartyFinderGui.ReceiveListing -= this.ReceiveListing; this.JoinPfHook?.Dispose(); this.RequestPfListingsHook?.Dispose(); } private void ReceiveListing(PartyFinderListing listing, PartyFinderListingEventArgs args) { if (args.BatchNumber != this.LastBatch) { this.Listings.Clear(); } this.LastBatch = args.BatchNumber; this.Listings[listing.Id] = listing; } private byte OnRequestPartyFinderListings(IntPtr agent, byte categoryIdx) { this.PartyFinderAgent = agent; return this.RequestPfListingsHook!.Original(agent, categoryIdx); } private IntPtr JoinPfDetour(IntPtr manager, IntPtr a2, int type, IntPtr packetData, uint a5) { // Updated: 5.5 const int idOffset = -0x20; var ret = this.JoinPfHook!.Original(manager, a2, type, packetData, a5); if (this.JoinParty == null || (JoinType) type != JoinType.PartyFinder || packetData == IntPtr.Zero) { return ret; } try { var id = (uint) Marshal.ReadInt32(packetData + idOffset); if (this.Listings.TryGetValue(id, out var listing)) { this.JoinParty?.Invoke(listing); } } catch (Exception ex) { PluginLog.LogError(ex, "Exception in PF join detour"); } return ret; } /// /// /// Refresh the Party Finder listings. This does not open the Party Finder. /// /// /// This maintains the currently selected category. /// /// /// If the hook is not enabled or if the signature for this function could not be found public void RefreshListings() { if (this.RequestPartyFinderListings == null) { throw new InvalidOperationException("Could not find signature for Party Finder listings"); } if (!this.ListingsEnabled) { throw new InvalidOperationException("PartyFinder hooks are not enabled"); } // Updated 5.5 const int categoryOffset = 10_655; if (this.PartyFinderAgent == IntPtr.Zero) { return; } var categoryIdx = Marshal.ReadByte(this.PartyFinderAgent + categoryOffset); this.RequestPartyFinderListings(this.PartyFinderAgent, categoryIdx); } } internal enum JoinType : byte { /// /// Join via invite or party conversion. /// Normal = 0, /// /// Join via Party Finder. /// PartyFinder = 1, Unknown2 = 2, /// /// Remain in cross-world party after leaving a duty. /// LeaveDuty = 3, Unknown4 = 4, } }