Files
HSRTools/HSRTools/Services/AutoAcceptPartyService.cs
T
jorg b1f01d2794 Auto-accept: debug logging, scan mode, friend/FC cache on login
- 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>
2026-02-21 12:48:29 -06:00

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;
}
}
}