2021-04-06 08:41:38 +00:00
using System ;
2021-04-22 17:54:32 +00:00
using System.Collections.Generic ;
2021-04-06 08:41:38 +00:00
using System.Runtime.InteropServices ;
using Dalamud.Game ;
2021-08-22 20:46:41 +00:00
using Dalamud.Game.Gui.PartyFinder.Types ;
2021-04-06 08:41:38 +00:00
using Dalamud.Hooking ;
2023-10-03 06:25:45 +00:00
using Dalamud.Plugin.Services ;
namespace XivCommon.Functions ;
/// <summary>
/// A class containing Party Finder functionality
/// </summary>
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 ?? ?? ?? ?? 41 0F B7 07 49 8B CC" ;
}
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 < RequestPartyFinderListingsDelegate > ? RequestPfListingsHook { get ; }
private Hook < JoinPfDelegate > ? JoinPfHook { get ; }
2021-04-06 08:41:38 +00:00
2021-04-11 14:13:10 +00:00
/// <summary>
2023-10-03 06:25:45 +00:00
/// The delegate for party join events.
2021-04-11 14:13:10 +00:00
/// </summary>
2023-10-03 06:25:45 +00:00
public delegate void JoinPfEventDelegate ( PartyFinderListing listing ) ;
2021-04-24 17:10:13 +00:00
2023-10-03 06:25:45 +00:00
/// <summary>
/// <para>
/// The event that is fired when the player joins a <b>cross-world</b> party via Party Finder.
/// </para>
/// <para>
/// Requires the <see cref="Hooks.PartyFinderJoins"/> hook to be enabled.
/// </para>
/// </summary>
public event JoinPfEventDelegate ? JoinParty ;
2021-05-29 05:22:31 +00:00
2023-10-03 06:25:45 +00:00
private IPartyFinderGui PartyFinderGui { get ; }
private bool JoinsEnabled { get ; }
private bool ListingsEnabled { get ; }
private IntPtr PartyFinderAgent { get ; set ; } = IntPtr . Zero ;
private Dictionary < uint , PartyFinderListing > Listings { get ; } = new ( ) ;
private int LastBatch { get ; set ; } = - 1 ;
2021-04-06 08:41:38 +00:00
2023-10-03 06:25:45 +00:00
/// <summary>
/// <para>
/// The current Party Finder listings that have been displayed.
/// </para>
/// <para>
/// This dictionary is cleared and updated each time the Party Finder is requested, and it only contains the category selected in the Party Finder addon.
/// </para>
/// <para>
/// Keys are the listing ID for fast lookup by ID. Values are the listing itself.
/// </para>
/// </summary>
public IReadOnlyDictionary < uint , PartyFinderListing > CurrentListings = > this . Listings ;
2021-04-11 13:24:56 +00:00
2023-10-03 06:25:45 +00:00
internal PartyFinder ( ISigScanner scanner , IPartyFinderGui partyFinderGui , IGameInteropProvider interop , Hooks hooks ) {
this . PartyFinderGui = partyFinderGui ;
2021-04-06 08:41:38 +00:00
2023-10-03 06:25:45 +00:00
this . ListingsEnabled = hooks . HasFlag ( Hooks . PartyFinderListings ) ;
this . JoinsEnabled = hooks . HasFlag ( Hooks . PartyFinderJoins ) ;
if ( this . ListingsEnabled | | this . JoinsEnabled ) {
this . PartyFinderGui . ReceiveListing + = this . ReceiveListing ;
2021-04-22 17:54:32 +00:00
}
2023-10-03 06:25:45 +00:00
if ( scanner . TryScanText ( Signatures . RequestListings , out var requestPfPtr , "Party Finder listings" ) ) {
this . RequestPartyFinderListings = Marshal . GetDelegateForFunctionPointer < RequestPartyFinderListingsDelegate > ( requestPfPtr ) ;
2021-04-22 17:54:32 +00:00
2023-10-03 06:25:45 +00:00
if ( this . ListingsEnabled ) {
this . RequestPfListingsHook = interop . HookFromAddress < RequestPartyFinderListingsDelegate > ( requestPfPtr , this . OnRequestPartyFinderListings ) ;
this . RequestPfListingsHook . Enable ( ) ;
}
}
2021-04-22 17:54:32 +00:00
2023-10-03 06:25:45 +00:00
if ( this . JoinsEnabled & & scanner . TryScanText ( Signatures . JoinCrossParty , out var joinPtr , "Party Finder joins" ) ) {
this . JoinPfHook = interop . HookFromAddress < JoinPfDelegate > ( joinPtr , this . JoinPfDetour ) ;
this . JoinPfHook . Enable ( ) ;
2021-04-06 08:41:38 +00:00
}
2023-10-03 06:25:45 +00:00
}
2021-04-06 08:41:38 +00:00
2023-10-03 06:25:45 +00:00
/// <inheritdoc />
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 ( ) ;
2021-04-06 08:41:38 +00:00
}
2023-10-03 06:25:45 +00:00
this . LastBatch = args . BatchNumber ;
2021-04-22 17:54:32 +00:00
2023-10-03 06:25:45 +00:00
this . Listings [ listing . Id ] = listing ;
}
2021-04-23 02:05:14 +00:00
2023-10-03 06:25:45 +00:00
private byte OnRequestPartyFinderListings ( IntPtr agent , byte categoryIdx ) {
this . PartyFinderAgent = agent ;
return this . RequestPfListingsHook ! . Original ( agent , categoryIdx ) ;
}
2021-04-22 17:54:32 +00:00
2023-10-03 06:25:45 +00:00
private IntPtr JoinPfDetour ( IntPtr manager , IntPtr a2 , int type , IntPtr packetData , uint a5 ) {
// Updated: 5.5
const int idOffset = - 0x20 ;
2021-04-22 17:54:32 +00:00
2023-10-03 06:25:45 +00:00
var ret = this . JoinPfHook ! . Original ( manager , a2 , type , packetData , a5 ) ;
if ( this . JoinParty = = null | | ( JoinType ) type ! = JoinType . PartyFinder | | packetData = = IntPtr . Zero ) {
2021-04-22 17:54:32 +00:00
return ret ;
}
2023-10-03 06:25:45 +00:00
try {
var id = ( uint ) Marshal . ReadInt32 ( packetData + idOffset ) ;
if ( this . Listings . TryGetValue ( id , out var listing ) ) {
this . JoinParty ? . Invoke ( listing ) ;
2021-04-24 17:10:13 +00:00
}
2023-10-03 06:25:45 +00:00
} catch ( Exception ex ) {
Logger . Log . Error ( ex , "Exception in PF join detour" ) ;
}
2021-04-24 17:10:13 +00:00
2023-10-03 06:25:45 +00:00
return ret ;
}
2021-04-11 13:24:56 +00:00
2023-10-03 06:25:45 +00:00
/// <summary>
/// <para>
/// Refresh the Party Finder listings. This does not open the Party Finder.
/// </para>
/// <para>
/// This maintains the currently selected category.
/// </para>
/// </summary>
/// <exception cref="InvalidOperationException">If the <see cref="Hooks.PartyFinderListings"/> hook is not enabled or if the signature for this function could not be found</exception>
public void RefreshListings ( ) {
if ( this . RequestPartyFinderListings = = null ) {
throw new InvalidOperationException ( "Could not find signature for Party Finder listings" ) ;
}
2021-04-06 08:41:38 +00:00
2023-10-03 06:25:45 +00:00
if ( ! this . ListingsEnabled ) {
throw new InvalidOperationException ( "PartyFinder hooks are not enabled" ) ;
}
// Updated 6.0
const int categoryOffset = 11_031 ;
2021-04-06 08:41:38 +00:00
2023-10-03 06:25:45 +00:00
if ( this . PartyFinderAgent = = IntPtr . Zero ) {
return ;
2021-04-06 08:41:38 +00:00
}
2023-10-03 06:25:45 +00:00
var categoryIdx = Marshal . ReadByte ( this . PartyFinderAgent + categoryOffset ) ;
this . RequestPartyFinderListings ( this . PartyFinderAgent , categoryIdx ) ;
2021-04-06 08:41:38 +00:00
}
2023-10-03 06:25:45 +00:00
}
2021-04-24 16:11:34 +00:00
2023-10-03 06:25:45 +00:00
internal enum JoinType : byte {
/// <summary>
/// Join via invite or party conversion.
/// </summary>
Normal = 0 ,
2021-04-24 16:11:34 +00:00
2023-10-03 06:25:45 +00:00
/// <summary>
/// Join via Party Finder.
/// </summary>
PartyFinder = 1 ,
2021-04-24 16:11:34 +00:00
2023-10-03 06:25:45 +00:00
Unknown2 = 2 ,
2021-04-24 16:11:34 +00:00
2023-10-03 06:25:45 +00:00
/// <summary>
/// Remain in cross-world party after leaving a duty.
/// </summary>
LeaveDuty = 3 ,
2021-04-24 16:11:34 +00:00
2023-10-03 06:25:45 +00:00
Unknown4 = 4 ,
2021-04-06 08:41:38 +00:00
}