feature/auto-accept-party-invites #2

Merged
KnackAtNite merged 2 commits from feature/auto-accept-party-invites into main 2026-02-21 21:28:35 +00:00
7 changed files with 521 additions and 14 deletions
Showing only changes of commit b1f01d2794 - Show all commits
+38
View File
@@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
namespace HSRTools.Configuration;
/// <summary>
/// Cached list of (Name, WorldId) for friends and FC members.
/// Persisted to disk so we don't need to open the in-game windows every time.
/// </summary>
public class FriendFcCache
{
/// <summary>Friend list: character name + home world ID.</summary>
public List<CachedPlayer> Friends { get; set; } = new();
/// <summary>Free Company members: character name + home world ID.</summary>
public List<CachedPlayer> FreeCompanyMembers { get; set; } = new();
/// <summary>When the cache was last refreshed (UTC).</summary>
public long LastUpdatedUtcTicks { get; set; }
public bool IsInFriends(string name, ushort worldId)
{
if (Friends == null || Friends.Count == 0) return false;
return Friends.Exists(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase) && p.WorldId == worldId);
}
public bool IsInFreeCompany(string name, ushort worldId)
{
if (FreeCompanyMembers == null || FreeCompanyMembers.Count == 0) return false;
return FreeCompanyMembers.Exists(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase) && p.WorldId == worldId);
}
}
public class CachedPlayer
{
public string Name { get; set; } = string.Empty;
public ushort WorldId { get; set; }
}
@@ -63,4 +63,18 @@ public class HSRToolsConfiguration
/// When true and AutoAcceptEnabled, accept party invites from members of your Free Company.
/// </summary>
public bool AutoAcceptFromFreeCompany { get; set; } = true;
/// <summary>
/// When true, logs every addon that opens. Use this to find the party invite addon name:
/// enable it, get a party invite, then check the log. Disable after finding the name.
/// Very noisy - only use briefly.
/// </summary>
public bool AutoAcceptScanAddons { get; set; } = false;
/// <summary>
/// When true, on each login the plugin will briefly open the Friends and Free Company
/// windows to cache names, then close them. The cache is saved so auto-accept works
/// without you opening those windows.
/// </summary>
public bool CacheFriendsAndFcOnLogin { get; set; } = true;
}
+19 -1
View File
@@ -15,6 +15,7 @@ public sealed class HSRToolsPlugin : IDalamudPlugin
private readonly string _configDir;
private readonly ChatMonitorService _chatMonitorService;
private readonly FriendFcCacheService _friendFcCacheService;
private readonly AutoAcceptPartyService _autoAcceptPartyService;
private readonly ConfigWindow _configWindow;
private readonly WindowSystem _windowSystem;
@@ -33,22 +34,32 @@ public sealed class HSRToolsPlugin : IDalamudPlugin
PluginServices.PluginLog,
_config);
_friendFcCacheService = new FriendFcCacheService(
_configDir,
PluginServices.PluginLog,
PluginServices.Framework,
PluginServices.ClientState,
_config);
_autoAcceptPartyService = new AutoAcceptPartyService(
PluginServices.AddonLifecycle,
PluginServices.PluginLog,
_config);
_config,
_friendFcCacheService);
_configWindow = new ConfigWindow(_config);
_windowSystem = new WindowSystem("HSRTools");
_windowSystem.AddWindow(_configWindow);
PluginServices.PluginInterface.UiBuilder.Draw += OnDraw;
PluginServices.Framework.Update += _friendFcCacheService.OnFrameworkUpdate;
PluginServices.PluginInterface.UiBuilder.OpenConfigUi += OpenConfigUi;
PluginServices.CommandManager.AddHandler("/hsr", new CommandInfo(OnHsrCommand)
{
HelpMessage = "Open HSRTools settings.",
});
_chatMonitorService.SetConfiguration(_config);
_friendFcCacheService.Start();
_chatMonitorService.Start();
_autoAcceptPartyService.SetConfiguration(_config);
_autoAcceptPartyService.Start();
@@ -56,7 +67,9 @@ public sealed class HSRToolsPlugin : IDalamudPlugin
public void Dispose()
{
PluginServices.Framework.Update -= _friendFcCacheService.OnFrameworkUpdate;
PluginServices.CommandManager.RemoveHandler("/hsr");
_friendFcCacheService.Stop();
_chatMonitorService.Stop();
_autoAcceptPartyService.Stop();
_windowSystem.RemoveAllWindows();
@@ -66,6 +79,11 @@ public sealed class HSRToolsPlugin : IDalamudPlugin
private void OnDraw()
{
_windowSystem.Draw();
if (_configWindow.IsOpen)
{
_autoAcceptPartyService.SetConfiguration(_config);
_friendFcCacheService.SetConfiguration(_config);
}
}
private void OpenConfigUi()
+108 -13
View File
@@ -15,119 +15,214 @@ namespace HSRTools.Services;
/// </summary>
public sealed class AutoAcceptPartyService
{
private const string PartyInviteAddonName = "PartyInvite";
// 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)
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, PartyInviteAddonName, OnPartyInviteAddon);
_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, PartyInviteAddonName, OnPartyInviteAddon);
_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 && IsFriend(inviterName, inviterWorldId);
var isFcMember = _config.AutoAcceptFromFreeCompany && IsFreeCompanyMember(inviterName, 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($"Auto-accepting party invite from {inviterName} ({reason}).");
_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($"Failed to auto-accept party invite from {inviterName}.");
_log.Warning($"[AutoAccept] Failed to auto-accept party invite from {inviterName}.");
}
catch (Exception ex)
{
_log.Error(ex, $"Error auto-accepting party invite from {inviterName}");
_log.Error(ex, $"[AutoAccept] Error auto-accepting party invite from {inviterName}");
}
}
}
private static unsafe bool IsFriend(string characterName, ushort worldId)
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
catch (Exception ex)
{
if (_config.Debug)
_log.Error(ex, "[AutoAccept] IsFriend exception");
return false;
}
}
private static unsafe bool IsFreeCompanyMember(string characterName, ushort worldId)
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
catch (Exception ex)
{
if (_config.Debug)
_log.Error(ex, "[AutoAccept] IsFreeCompanyMember exception");
return false;
}
}
+326
View File
@@ -0,0 +1,326 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Client.UI.Info;
using HSRTools.Configuration;
namespace HSRTools.Services;
/// <summary>
/// On login, opens the Friends and FC roster addons briefly to scrape names into a cache,
/// then closes them. The cache is persisted so auto-accept can use it without opening the UIs again.
/// </summary>
public sealed class FriendFcCacheService
{
private const string CacheFileName = "HSRToolsFriendFcCache.json";
private const int FramesToWaitAfterOpen = 120; // ~2 seconds for friend list
private const int FramesToWaitForFc = 240; // ~4 seconds FC roster often loads slower
private const int NameOffsetInCharacterData = 0x32;
private const int NameMaxBytes = 32;
private readonly string _configDir;
private readonly IPluginLog _log;
private readonly IFramework _framework;
private readonly IClientState _clientState;
private HSRToolsConfiguration _config;
private FriendFcCache _cache = new();
private readonly object _cacheLock = new();
private bool _loginHandled;
private int _scrapePhase; // 0=idle, 1=delay then open friends, 2=friends open (wait), 3=scrape friends & open FC, 4=FC open (wait), 5=scrape FC & done
private int _scrapeTicksLeft;
public FriendFcCacheService(
string configDir,
IPluginLog log,
IFramework framework,
IClientState clientState,
HSRToolsConfiguration config)
{
_configDir = configDir;
_log = log;
_framework = framework;
_clientState = clientState;
_config = config;
}
public void SetConfiguration(HSRToolsConfiguration config)
{
_config = config;
}
public void Start()
{
LoadCache();
_clientState.Login += OnLogin;
_clientState.Logout += OnLogout;
}
public void Stop()
{
_clientState.Login -= OnLogin;
_clientState.Logout -= OnLogout;
_scrapePhase = 0;
}
/// <summary>Returns a snapshot of the cache for reading. Prefer using IsInFriends/IsInFreeCompany.</summary>
public FriendFcCache GetCacheSnapshot()
{
lock (_cacheLock)
{
return new FriendFcCache
{
Friends = _cache.Friends == null ? new List<CachedPlayer>() : new List<CachedPlayer>(_cache.Friends),
FreeCompanyMembers = _cache.FreeCompanyMembers == null ? new List<CachedPlayer>() : new List<CachedPlayer>(_cache.FreeCompanyMembers),
LastUpdatedUtcTicks = _cache.LastUpdatedUtcTicks,
};
}
}
public bool IsInFriends(string name, ushort worldId)
{
lock (_cacheLock) { return _cache.IsInFriends(name, worldId); }
}
public bool IsInFreeCompany(string name, ushort worldId)
{
lock (_cacheLock) { return _cache.IsInFreeCompany(name, worldId); }
}
public bool HasCachedData()
{
lock (_cacheLock)
{
return (_cache.Friends?.Count ?? 0) > 0 || (_cache.FreeCompanyMembers?.Count ?? 0) > 0;
}
}
private void OnLogin()
{
_loginHandled = false;
}
private void OnLogout(int type, int code)
{
_loginHandled = false;
_scrapePhase = 0;
}
/// <summary>Called each frame from plugin; runs delayed scrape after login.</summary>
public void OnFrameworkUpdate(IFramework framework)
{
if (_scrapePhase > 0)
{
TickScrapePhase();
return;
}
if (!_config.CacheFriendsAndFcOnLogin || _loginHandled)
return;
if (!_clientState.IsLoggedIn)
return;
_loginHandled = true;
_log.Info("[Cache] Login detected. Will open Friends and FC windows to refresh cache.");
_scrapePhase = 1;
_scrapeTicksLeft = 60;
}
private void TickScrapePhase()
{
if (_scrapeTicksLeft > 0)
{
_scrapeTicksLeft--;
return;
}
unsafe
{
switch (_scrapePhase)
{
case 1:
var friendAgent = AgentFriendlist.Instance();
if (friendAgent == null)
{
_log.Warning("[Cache] AgentFriendlist not found.");
_scrapePhase = 3;
_scrapeTicksLeft = 0;
TickScrapePhase();
return;
}
((AgentInterface*)friendAgent)->ShowAddon();
_scrapePhase = 2;
_scrapeTicksLeft = FramesToWaitAfterOpen;
break;
case 2:
if (AgentFriendlist.Instance() != null)
((AgentInterface*)AgentFriendlist.Instance())->HideAddon();
var friendList = ScrapeProxy(InfoProxyId.FriendList);
lock (_cacheLock)
{
_cache.Friends = friendList;
_log.Info($"[Cache] Cached {friendList.Count} friends.");
}
var fcAgent = AgentFreeCompany.Instance();
if (fcAgent == null)
{
_log.Warning("[Cache] AgentFreeCompany not found.");
lock (_cacheLock) { _cache.LastUpdatedUtcTicks = DateTime.UtcNow.Ticks; }
SaveCache();
_scrapePhase = 0;
return;
}
((AgentInterface*)fcAgent)->ShowAddon();
RequestFcMemberData();
_scrapePhase = 4;
_scrapeTicksLeft = FramesToWaitForFc;
break;
case 3:
fcAgent = AgentFreeCompany.Instance();
if (fcAgent == null)
{
_log.Warning("[Cache] AgentFreeCompany not found.");
SaveCache();
_scrapePhase = 0;
return;
}
((AgentInterface*)fcAgent)->ShowAddon();
RequestFcMemberData();
_scrapePhase = 4;
_scrapeTicksLeft = FramesToWaitForFc;
break;
case 4:
if (AgentFreeCompany.Instance() != null)
((AgentInterface*)AgentFreeCompany.Instance())->HideAddon();
var fcList = ScrapeProxy(InfoProxyId.FreeCompanyMember);
lock (_cacheLock)
{
_cache.FreeCompanyMembers = fcList;
_cache.LastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
if (fcList.Count == 0)
_log.Info("[Cache] Cached 0 FC members (not in an FC, or roster not loaded yet). Cache refresh done.");
else
_log.Info($"[Cache] Cached {fcList.Count} FC members. Cache refresh done.");
}
SaveCache();
_scrapePhase = 0;
break;
}
}
}
/// <summary>Request the FC member list from the server; call after opening the FC addon.</summary>
private static unsafe void RequestFcMemberData()
{
try
{
var infoModule = InfoModule.Instance();
if (infoModule == null) return;
var proxy = infoModule->GetInfoProxyById(InfoProxyId.FreeCompanyMember);
if (proxy == null) return;
proxy->RequestData();
}
catch
{
// ignore
}
}
private static unsafe List<CachedPlayer> ScrapeProxy(InfoProxyId proxyId)
{
var result = new List<CachedPlayer>();
try
{
var infoModule = InfoModule.Instance();
if (infoModule == null) return result;
var proxy = (InfoProxyCommonList*)infoModule->GetInfoProxyById(proxyId);
if (proxy == null) return result;
var count = ((InfoProxyInterface*)proxy)->GetEntryCount();
if (count == 0 || proxy->CharData == null) return result;
for (uint i = 0; i < count; i++)
{
var entry = proxy->GetEntry(i);
if (entry == null) continue;
var name = ReadCharacterName(entry);
if (string.IsNullOrWhiteSpace(name)) continue;
result.Add(new CachedPlayer { Name = name.Trim(), WorldId = entry->HomeWorld });
}
}
catch
{
// ignore
}
return result;
}
private static unsafe string ReadCharacterName(InfoProxyCommonList.CharacterData* entry)
{
if (entry == null) return string.Empty;
try
{
var ptr = (byte*)entry + NameOffsetInCharacterData;
var span = new ReadOnlySpan<byte>(ptr, NameMaxBytes);
var end = span.IndexOf((byte)0);
var len = end < 0 ? NameMaxBytes : end;
return Encoding.UTF8.GetString(span.Slice(0, len));
}
catch
{
return string.Empty;
}
}
private void LoadCache()
{
var path = Path.Combine(_configDir, CacheFileName);
if (!File.Exists(path)) return;
try
{
var json = File.ReadAllText(path);
var loaded = JsonSerializer.Deserialize<FriendFcCache>(json);
if (loaded != null)
{
lock (_cacheLock)
{
_cache.Friends = loaded.Friends ?? new List<CachedPlayer>();
_cache.FreeCompanyMembers = loaded.FreeCompanyMembers ?? new List<CachedPlayer>();
_cache.LastUpdatedUtcTicks = loaded.LastUpdatedUtcTicks;
}
_log.Info($"[Cache] Loaded {_cache.Friends.Count} friends, {_cache.FreeCompanyMembers.Count} FC members from disk.");
}
}
catch (Exception ex)
{
_log.Warning(ex, "[Cache] Failed to load cache file.");
}
}
private void SaveCache()
{
FriendFcCache snapshot;
lock (_cacheLock)
{
snapshot = new FriendFcCache
{
Friends = new List<CachedPlayer>(_cache.Friends ?? new List<CachedPlayer>()),
FreeCompanyMembers = new List<CachedPlayer>(_cache.FreeCompanyMembers ?? new List<CachedPlayer>()),
LastUpdatedUtcTicks = _cache.LastUpdatedUtcTicks,
};
}
var path = Path.Combine(_configDir, CacheFileName);
try
{
Directory.CreateDirectory(_configDir);
var json = JsonSerializer.Serialize(snapshot, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(path, json);
}
catch (Exception ex)
{
_log.Warning(ex, "[Cache] Failed to save cache file.");
}
}
}
+2
View File
@@ -14,4 +14,6 @@ public sealed class PluginServices
[PluginService] public static IToastGui ToastGui { get; private set; } = null!;
[PluginService] public static ICommandManager CommandManager { get; private set; } = null!;
[PluginService] public static IAddonLifecycle AddonLifecycle { get; private set; } = null!;
[PluginService] public static IClientState ClientState { get; private set; } = null!;
[PluginService] public static IFramework Framework { get; private set; } = null!;
}
+14
View File
@@ -18,6 +18,8 @@ public sealed class ConfigWindow : Window
private bool _autoAcceptEnabled;
private bool _autoAcceptFromFriends;
private bool _autoAcceptFromFreeCompany;
private bool _autoAcceptScanAddons;
private bool _cacheFriendsAndFcOnLogin;
public ConfigWindow(HSRToolsConfiguration config)
: base("HSRTools Configuration", ImGuiWindowFlags.AlwaysAutoResize)
@@ -39,6 +41,8 @@ public sealed class ConfigWindow : Window
_autoAcceptEnabled = _config.AutoAcceptEnabled;
_autoAcceptFromFriends = _config.AutoAcceptFromFriends;
_autoAcceptFromFreeCompany = _config.AutoAcceptFromFreeCompany;
_autoAcceptScanAddons = _config.AutoAcceptScanAddons;
_cacheFriendsAndFcOnLogin = _config.CacheFriendsAndFcOnLogin;
}
public override void Draw()
@@ -56,6 +60,8 @@ public sealed class ConfigWindow : Window
_config.AutoAcceptEnabled = false;
_config.AutoAcceptFromFriends = true;
_config.AutoAcceptFromFreeCompany = true;
_config.AutoAcceptScanAddons = false;
_config.CacheFriendsAndFcOnLogin = true;
SyncFromConfig();
}
@@ -100,6 +106,14 @@ public sealed class ConfigWindow : Window
if (ImGui.Checkbox(" From Free Company members", ref _autoAcceptFromFreeCompany))
_config.AutoAcceptFromFreeCompany = _autoAcceptFromFreeCompany;
}
if (ImGui.Checkbox("Scan mode: log all addons that open", ref _autoAcceptScanAddons))
_config.AutoAcceptScanAddons = _autoAcceptScanAddons;
if (_autoAcceptScanAddons)
ImGui.TextColored(new System.Numerics.Vector4(1, 0.8f, 0, 1), " Enable, get a party invite, check log for addon name. Disable after.");
if (ImGui.Checkbox("Cache friends & FC on login", ref _cacheFriendsAndFcOnLogin))
_config.CacheFriendsAndFcOnLogin = _cacheFriendsAndFcOnLogin;
if (_cacheFriendsAndFcOnLogin)
ImGui.TextColored(new System.Numerics.Vector4(0.7f, 0.7f, 0.7f, 1f), " Opens Friends and FC windows briefly on login to cache names, then closes them.");
ImGui.Unindent();
}
}