b1f01d2794
- Add debug logging (gated by Debug) and scan mode to find party-invite addon - Listen for PartyInvite and SelectYesno addons - Cache friends and FC members on login: open Friends/FC addons briefly, scrape names, close; persist cache to HSRToolsFriendFcCache.json - Auto-accept checks cache first, then live proxy (works without opening UIs) - Config: CacheFriendsAndFcOnLogin, AutoAcceptScanAddons - FC: RequestData() + longer wait; clearer log when 0 FC members Co-authored-by: Cursor <cursoragent@cursor.com>
230 lines
8.7 KiB
C#
230 lines
8.7 KiB
C#
using System;
|
|
using Dalamud.Game.Addon.Lifecycle;
|
|
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
|
using Dalamud.Plugin.Services;
|
|
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
|
using FFXIVClientStructs.FFXIV.Client.UI.Info;
|
|
using HSRTools.Configuration;
|
|
|
|
namespace HSRTools.Services;
|
|
|
|
/// <summary>
|
|
/// Automatically accepts party invites when the inviter is a friend or Free Company member.
|
|
/// Uses IAddonLifecycle to detect when the party invite addon opens, then checks the
|
|
/// inviter against the friend list and FC roster before accepting.
|
|
/// </summary>
|
|
public sealed class AutoAcceptPartyService
|
|
{
|
|
// Party invite may use PartyInvite or SelectYesno (common confirmation dialog) - listen to both
|
|
private static readonly string[] PartyInviteAddonNames = ["PartyInvite", "SelectYesno"];
|
|
|
|
private readonly IAddonLifecycle _addonLifecycle;
|
|
private readonly IPluginLog _log;
|
|
private readonly FriendFcCacheService _cacheService;
|
|
private HSRToolsConfiguration _config;
|
|
private bool _scanListenerRegistered;
|
|
|
|
public AutoAcceptPartyService(IAddonLifecycle addonLifecycle, IPluginLog log, HSRToolsConfiguration config, FriendFcCacheService cacheService)
|
|
{
|
|
_addonLifecycle = addonLifecycle;
|
|
_log = log;
|
|
_config = config;
|
|
_cacheService = cacheService;
|
|
}
|
|
|
|
public void SetConfiguration(HSRToolsConfiguration config)
|
|
{
|
|
var wasScanning = _scanListenerRegistered;
|
|
_config = config;
|
|
if (wasScanning && !_config.AutoAcceptScanAddons)
|
|
UnregisterScanListener();
|
|
else if (!wasScanning && _config.AutoAcceptScanAddons)
|
|
RegisterScanListener();
|
|
}
|
|
|
|
public void Start()
|
|
{
|
|
_addonLifecycle.RegisterListener(AddonEvent.PreSetup, PartyInviteAddonNames, OnPartyInviteAddon);
|
|
_log.Info($"[AutoAccept] Listening for party invites (addons: {string.Join(", ", PartyInviteAddonNames)}). Enable Debug for detailed logs.");
|
|
if (_config.AutoAcceptScanAddons)
|
|
RegisterScanListener();
|
|
}
|
|
|
|
public void Stop()
|
|
{
|
|
_addonLifecycle.UnregisterListener(AddonEvent.PreSetup, PartyInviteAddonNames, OnPartyInviteAddon);
|
|
UnregisterScanListener();
|
|
}
|
|
|
|
private void RegisterScanListener()
|
|
{
|
|
if (_scanListenerRegistered) return;
|
|
_addonLifecycle.RegisterListener(AddonEvent.PreSetup, OnAnyAddonScan);
|
|
_scanListenerRegistered = true;
|
|
_log.Info("[AutoAccept] SCAN MODE: Logging every addon that opens. Get a party invite and check the log for the addon name.");
|
|
}
|
|
|
|
private void UnregisterScanListener()
|
|
{
|
|
if (!_scanListenerRegistered) return;
|
|
_addonLifecycle.UnregisterListener(AddonEvent.PreSetup, OnAnyAddonScan);
|
|
_scanListenerRegistered = false;
|
|
}
|
|
|
|
private void OnAnyAddonScan(AddonEvent eventType, AddonArgs args)
|
|
{
|
|
if (!_config.AutoAcceptScanAddons) return;
|
|
_log.Info($"[AutoAccept] SCAN: Addon opened: '{args.AddonName}'");
|
|
}
|
|
|
|
private void OnPartyInviteAddon(AddonEvent eventType, AddonArgs args)
|
|
{
|
|
if (_config.Debug)
|
|
_log.Info("[AutoAccept] Party invite addon triggered.");
|
|
|
|
if (!_config.AutoAcceptEnabled)
|
|
{
|
|
if (_config.Debug)
|
|
_log.Info("[AutoAccept] Skipped: Auto-accept is disabled.");
|
|
return;
|
|
}
|
|
|
|
if (!_config.AutoAcceptFromFriends && !_config.AutoAcceptFromFreeCompany)
|
|
{
|
|
if (_config.Debug)
|
|
_log.Info("[AutoAccept] Skipped: Both friends and FC options are disabled.");
|
|
return;
|
|
}
|
|
|
|
unsafe
|
|
{
|
|
var agent = AgentPartyInvite.Instance();
|
|
if (agent == null)
|
|
{
|
|
if (_config.Debug)
|
|
_log.Warning("[AutoAccept] AgentPartyInvite.Instance() is null.");
|
|
return;
|
|
}
|
|
|
|
var proxy = agent->InfoProxyPartyInvite;
|
|
if (proxy == null)
|
|
{
|
|
if (_config.Debug)
|
|
_log.Warning("[AutoAccept] InfoProxyPartyInvite is null.");
|
|
return;
|
|
}
|
|
|
|
var inviterName = proxy->InviterName.ToString();
|
|
if (string.IsNullOrWhiteSpace(inviterName))
|
|
{
|
|
if (_config.Debug)
|
|
_log.Warning("[AutoAccept] Inviter name is null or empty.");
|
|
return;
|
|
}
|
|
|
|
var inviterWorldId = proxy->InviterWorldId;
|
|
if (_config.Debug)
|
|
_log.Info($"[AutoAccept] Inviter: '{inviterName}' (worldId={inviterWorldId})");
|
|
|
|
var isFriend = _config.AutoAcceptFromFriends && (IsFriendCached(inviterName, inviterWorldId) || IsFriend(inviterName, inviterWorldId));
|
|
var isFcMember = _config.AutoAcceptFromFreeCompany && (IsFreeCompanyMemberCached(inviterName, inviterWorldId) || IsFreeCompanyMember(inviterName, inviterWorldId));
|
|
|
|
if (_config.Debug)
|
|
_log.Info($"[AutoAccept] IsFriend={isFriend}, IsFcMember={isFcMember} (FriendsEnabled={_config.AutoAcceptFromFriends}, FCEnabled={_config.AutoAcceptFromFreeCompany})");
|
|
|
|
if (!isFriend && !isFcMember)
|
|
{
|
|
if (_config.Debug)
|
|
_log.Info("[AutoAccept] Skipped: Inviter is not a friend or FC member.");
|
|
return;
|
|
}
|
|
|
|
var reason = isFriend && isFcMember ? "friend and FC member" : isFriend ? "friend" : "FC member";
|
|
_log.Info($"[AutoAccept] Auto-accepting party invite from {inviterName} ({reason}).");
|
|
|
|
try
|
|
{
|
|
// RespondToInvitation is on InfoProxyInvitedList (parent of InfoProxyPartyInvite)
|
|
var accepted = ((InfoProxyInvitedList*)proxy)->RespondToInvitation(inviterName, true);
|
|
if (_config.Debug)
|
|
_log.Info($"[AutoAccept] RespondToInvitation returned: {accepted}");
|
|
if (!accepted)
|
|
_log.Warning($"[AutoAccept] Failed to auto-accept party invite from {inviterName}.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.Error(ex, $"[AutoAccept] Error auto-accepting party invite from {inviterName}");
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool IsFriendCached(string characterName, ushort worldId) => _cacheService.IsInFriends(characterName, worldId);
|
|
private bool IsFreeCompanyMemberCached(string characterName, ushort worldId) => _cacheService.IsInFreeCompany(characterName, worldId);
|
|
|
|
private unsafe bool IsFriend(string characterName, ushort worldId)
|
|
{
|
|
try
|
|
{
|
|
var infoModule = InfoModule.Instance();
|
|
if (infoModule == null)
|
|
{
|
|
if (_config.Debug)
|
|
_log.Warning("[AutoAccept] IsFriend: InfoModule.Instance() is null.");
|
|
return false;
|
|
}
|
|
|
|
var proxy = (InfoProxyCommonList*)infoModule->GetInfoProxyById(InfoProxyId.FriendList);
|
|
if (proxy == null)
|
|
{
|
|
if (_config.Debug)
|
|
_log.Warning("[AutoAccept] IsFriend: FriendList proxy is null.");
|
|
return false;
|
|
}
|
|
|
|
var entry = proxy->GetEntryByName(characterName, worldId);
|
|
if (_config.Debug)
|
|
_log.Info($"[AutoAccept] IsFriend('{characterName}', worldId={worldId}): entry={(entry != null ? "found" : "null")}");
|
|
return entry != null;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (_config.Debug)
|
|
_log.Error(ex, "[AutoAccept] IsFriend exception");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private unsafe bool IsFreeCompanyMember(string characterName, ushort worldId)
|
|
{
|
|
try
|
|
{
|
|
var infoModule = InfoModule.Instance();
|
|
if (infoModule == null)
|
|
{
|
|
if (_config.Debug)
|
|
_log.Warning("[AutoAccept] IsFreeCompanyMember: InfoModule.Instance() is null.");
|
|
return false;
|
|
}
|
|
|
|
var proxy = (InfoProxyCommonList*)infoModule->GetInfoProxyById(InfoProxyId.FreeCompanyMember);
|
|
if (proxy == null)
|
|
{
|
|
if (_config.Debug)
|
|
_log.Warning("[AutoAccept] IsFreeCompanyMember: FreeCompanyMember proxy is null.");
|
|
return false;
|
|
}
|
|
|
|
var entry = proxy->GetEntryByName(characterName, worldId);
|
|
if (_config.Debug)
|
|
_log.Info($"[AutoAccept] IsFreeCompanyMember('{characterName}', worldId={worldId}): entry={(entry != null ? "found" : "null")}");
|
|
return entry != null;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (_config.Debug)
|
|
_log.Error(ex, "[AutoAccept] IsFreeCompanyMember exception");
|
|
return false;
|
|
}
|
|
}
|
|
}
|