Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 58e7d2ee46 | |||
| 804a43ebf3 | |||
| b1f01d2794 | |||
| 4dcea5d7de |
@@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
|
## [1.0.5] - 2025-02-21
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Exact match option** – When enabled, the entire message must exactly match the trigger (e.g. `!` triggers only on messages that are just `!`, not `hello!`).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Cross-world friends for auto-accept** – Auto-accept now works when a friend invites you from another world or while visiting another world (fallback to name-only match when world ID differs).
|
||||||
|
|
||||||
|
## [1.0.4] - 2025-02-20
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Auto-accept party invites** – Option to automatically accept party invites when the inviter is on your friend list or is a member of your Free Company. Configurable toggles for friends and FC members.
|
||||||
|
|
||||||
## [1.0.3] - 2025-02-03
|
## [1.0.3] - 2025-02-03
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Name-only match for cross-world friends (e.g. friend visiting another world).</summary>
|
||||||
|
public bool IsInFriendsByName(string name)
|
||||||
|
{
|
||||||
|
if (Friends == null || Friends.Count == 0) return false;
|
||||||
|
return Friends.Exists(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
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; }
|
||||||
|
}
|
||||||
@@ -15,6 +15,12 @@ public class HSRToolsConfiguration
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool CaseSensitive { get; set; } = false;
|
public bool CaseSensitive { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When true, the entire message must exactly match the trigger (after trimming).
|
||||||
|
/// When false, the message only needs to contain the trigger anywhere.
|
||||||
|
/// </summary>
|
||||||
|
public bool TriggerExactMatch { get; set; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether to monitor Free Company chat.
|
/// Whether to monitor Free Company chat.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -45,4 +51,36 @@ public class HSRToolsConfiguration
|
|||||||
/// Use this to diagnose cross-world invite issues.
|
/// Use this to diagnose cross-world invite issues.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Debug { get; set; } = false;
|
public bool Debug { get; set; } = false;
|
||||||
|
|
||||||
|
// --- Auto-accept party invites ---
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When true, automatically accept party invites from friends and/or Free Company members
|
||||||
|
/// (based on the options below).
|
||||||
|
/// </summary>
|
||||||
|
public bool AutoAcceptEnabled { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When true and AutoAcceptEnabled, accept party invites from players on your friend list.
|
||||||
|
/// </summary>
|
||||||
|
public bool AutoAcceptFromFriends { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Dalamud.NET.Sdk/14.0.1">
|
<Project Sdk="Dalamud.NET.Sdk/14.0.1">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
<Version>1.0.3.0</Version>
|
<Version>1.0.5.0</Version>
|
||||||
<Author>Knack117</Author>
|
<Author>Knack117</Author>
|
||||||
<Name>HSRTools</Name>
|
<Name>HSRTools</Name>
|
||||||
<InternalName>HSRTools</InternalName>
|
<InternalName>HSRTools</InternalName>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"Author": "Knack117",
|
"Author": "Knack117",
|
||||||
"Name": "HSRTools",
|
"Name": "HSRTools",
|
||||||
"Punchline": "Auto-invite to party when a trigger word is said in FC, LS, CWLS, or tells.",
|
"Punchline": "Auto-invite and auto-accept party invites from friends/FC.",
|
||||||
"Description": "Detects a user-defined trigger word in Free Company, Link Shell, Cross-World Link Shell, and tell chat, then invites the sender to your party. Works for same-world and cross-world (CWLS).",
|
"Description": "Detects a trigger word in FC, LS, CWLS, and tells to auto-invite; optionally auto-accepts party invites from friends and FC members.",
|
||||||
"RepoUrl": "https://github.com/Knack117/HSRTools",
|
"RepoUrl": "https://github.com/Knack117/HSRTools",
|
||||||
"Tags": [ "chat", "party", "invite", "automation" ],
|
"Tags": [ "chat", "party", "invite", "automation" ],
|
||||||
"AcceptsFeedback": true
|
"AcceptsFeedback": true
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ public sealed class HSRToolsPlugin : IDalamudPlugin
|
|||||||
|
|
||||||
private readonly string _configDir;
|
private readonly string _configDir;
|
||||||
private readonly ChatMonitorService _chatMonitorService;
|
private readonly ChatMonitorService _chatMonitorService;
|
||||||
|
private readonly FriendFcCacheService _friendFcCacheService;
|
||||||
|
private readonly AutoAcceptPartyService _autoAcceptPartyService;
|
||||||
private readonly ConfigWindow _configWindow;
|
private readonly ConfigWindow _configWindow;
|
||||||
private readonly WindowSystem _windowSystem;
|
private readonly WindowSystem _windowSystem;
|
||||||
private HSRToolsConfiguration _config;
|
private HSRToolsConfiguration _config;
|
||||||
@@ -32,24 +34,44 @@ public sealed class HSRToolsPlugin : IDalamudPlugin
|
|||||||
PluginServices.PluginLog,
|
PluginServices.PluginLog,
|
||||||
_config);
|
_config);
|
||||||
|
|
||||||
|
_friendFcCacheService = new FriendFcCacheService(
|
||||||
|
_configDir,
|
||||||
|
PluginServices.PluginLog,
|
||||||
|
PluginServices.Framework,
|
||||||
|
PluginServices.ClientState,
|
||||||
|
_config);
|
||||||
|
|
||||||
|
_autoAcceptPartyService = new AutoAcceptPartyService(
|
||||||
|
PluginServices.AddonLifecycle,
|
||||||
|
PluginServices.PluginLog,
|
||||||
|
_config,
|
||||||
|
_friendFcCacheService);
|
||||||
|
|
||||||
_configWindow = new ConfigWindow(_config);
|
_configWindow = new ConfigWindow(_config);
|
||||||
_windowSystem = new WindowSystem("HSRTools");
|
_windowSystem = new WindowSystem("HSRTools");
|
||||||
_windowSystem.AddWindow(_configWindow);
|
_windowSystem.AddWindow(_configWindow);
|
||||||
|
|
||||||
PluginServices.PluginInterface.UiBuilder.Draw += OnDraw;
|
PluginServices.PluginInterface.UiBuilder.Draw += OnDraw;
|
||||||
|
PluginServices.Framework.Update += _friendFcCacheService.OnFrameworkUpdate;
|
||||||
PluginServices.PluginInterface.UiBuilder.OpenConfigUi += OpenConfigUi;
|
PluginServices.PluginInterface.UiBuilder.OpenConfigUi += OpenConfigUi;
|
||||||
PluginServices.CommandManager.AddHandler("/hsr", new CommandInfo(OnHsrCommand)
|
PluginServices.CommandManager.AddHandler("/hsr", new CommandInfo(OnHsrCommand)
|
||||||
{
|
{
|
||||||
HelpMessage = "Open HSRTools settings.",
|
HelpMessage = "Open HSRTools settings.",
|
||||||
});
|
});
|
||||||
_chatMonitorService.SetConfiguration(_config);
|
_chatMonitorService.SetConfiguration(_config);
|
||||||
|
_friendFcCacheService.Start();
|
||||||
_chatMonitorService.Start();
|
_chatMonitorService.Start();
|
||||||
|
_autoAcceptPartyService.SetConfiguration(_config);
|
||||||
|
_autoAcceptPartyService.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
PluginServices.Framework.Update -= _friendFcCacheService.OnFrameworkUpdate;
|
||||||
PluginServices.CommandManager.RemoveHandler("/hsr");
|
PluginServices.CommandManager.RemoveHandler("/hsr");
|
||||||
|
_friendFcCacheService.Stop();
|
||||||
_chatMonitorService.Stop();
|
_chatMonitorService.Stop();
|
||||||
|
_autoAcceptPartyService.Stop();
|
||||||
_windowSystem.RemoveAllWindows();
|
_windowSystem.RemoveAllWindows();
|
||||||
SaveConfig(_configDir, _config);
|
SaveConfig(_configDir, _config);
|
||||||
}
|
}
|
||||||
@@ -57,6 +79,11 @@ public sealed class HSRToolsPlugin : IDalamudPlugin
|
|||||||
private void OnDraw()
|
private void OnDraw()
|
||||||
{
|
{
|
||||||
_windowSystem.Draw();
|
_windowSystem.Draw();
|
||||||
|
if (_configWindow.IsOpen)
|
||||||
|
{
|
||||||
|
_autoAcceptPartyService.SetConfiguration(_config);
|
||||||
|
_friendFcCacheService.SetConfiguration(_config);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OpenConfigUi()
|
private void OpenConfigUi()
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
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) || IsFriendCachedByName(inviterName));
|
||||||
|
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 IsFriendCachedByName(string characterName) => _cacheService.IsInFriendsByName(characterName);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -150,8 +150,16 @@ public sealed class ChatMonitorService
|
|||||||
var trigger = _config.TriggerText;
|
var trigger = _config.TriggerText;
|
||||||
var comparison = _config.CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
|
var comparison = _config.CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
|
||||||
|
|
||||||
|
if (_config.TriggerExactMatch)
|
||||||
|
{
|
||||||
|
if (!messageText.Trim().Equals(trigger, comparison))
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
if (!messageText.Contains(trigger, comparison))
|
if (!messageText.Contains(trigger, comparison))
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Read log module first for non-tells (sender data only valid at callback start; overwritten quickly).
|
// Read log module first for non-tells (sender data only valid at callback start; overwritten quickly).
|
||||||
var (logContentId, logWorldId) = type != XivChatType.TellIncoming
|
var (logContentId, logWorldId) = type != XivChatType.TellIncoming
|
||||||
|
|||||||
@@ -0,0 +1,332 @@
|
|||||||
|
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); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Name-only match for cross-world friends (friend visiting another world).</summary>
|
||||||
|
public bool IsInFriendsByName(string name)
|
||||||
|
{
|
||||||
|
lock (_cacheLock) { return _cache.IsInFriendsByName(name); }
|
||||||
|
}
|
||||||
|
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,4 +13,7 @@ public sealed class PluginServices
|
|||||||
[PluginService] public static IDalamudPluginInterface PluginInterface { get; private set; } = null!;
|
[PluginService] public static IDalamudPluginInterface PluginInterface { get; private set; } = null!;
|
||||||
[PluginService] public static IToastGui ToastGui { get; private set; } = null!;
|
[PluginService] public static IToastGui ToastGui { get; private set; } = null!;
|
||||||
[PluginService] public static ICommandManager CommandManager { 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!;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,18 @@ public sealed class ConfigWindow : Window
|
|||||||
private readonly HSRToolsConfiguration _config;
|
private readonly HSRToolsConfiguration _config;
|
||||||
private string _triggerTextInput = string.Empty;
|
private string _triggerTextInput = string.Empty;
|
||||||
private bool _caseSensitive;
|
private bool _caseSensitive;
|
||||||
|
private bool _triggerExactMatch;
|
||||||
private bool _monitorFreeCompany;
|
private bool _monitorFreeCompany;
|
||||||
private bool _monitorLinkShell;
|
private bool _monitorLinkShell;
|
||||||
private bool _monitorCrossWorldLinkShell;
|
private bool _monitorCrossWorldLinkShell;
|
||||||
private bool _monitorTell;
|
private bool _monitorTell;
|
||||||
private bool _enabled;
|
private bool _enabled;
|
||||||
private bool _debug;
|
private bool _debug;
|
||||||
|
private bool _autoAcceptEnabled;
|
||||||
|
private bool _autoAcceptFromFriends;
|
||||||
|
private bool _autoAcceptFromFreeCompany;
|
||||||
|
private bool _autoAcceptScanAddons;
|
||||||
|
private bool _cacheFriendsAndFcOnLogin;
|
||||||
|
|
||||||
public ConfigWindow(HSRToolsConfiguration config)
|
public ConfigWindow(HSRToolsConfiguration config)
|
||||||
: base("HSRTools Configuration", ImGuiWindowFlags.AlwaysAutoResize)
|
: base("HSRTools Configuration", ImGuiWindowFlags.AlwaysAutoResize)
|
||||||
@@ -27,12 +33,18 @@ public sealed class ConfigWindow : Window
|
|||||||
{
|
{
|
||||||
_triggerTextInput = _config.TriggerText ?? string.Empty;
|
_triggerTextInput = _config.TriggerText ?? string.Empty;
|
||||||
_caseSensitive = _config.CaseSensitive;
|
_caseSensitive = _config.CaseSensitive;
|
||||||
|
_triggerExactMatch = _config.TriggerExactMatch;
|
||||||
_monitorFreeCompany = _config.MonitorFreeCompany;
|
_monitorFreeCompany = _config.MonitorFreeCompany;
|
||||||
_monitorLinkShell = _config.MonitorLinkShell;
|
_monitorLinkShell = _config.MonitorLinkShell;
|
||||||
_monitorCrossWorldLinkShell = _config.MonitorCrossWorldLinkShell;
|
_monitorCrossWorldLinkShell = _config.MonitorCrossWorldLinkShell;
|
||||||
_monitorTell = _config.MonitorTell;
|
_monitorTell = _config.MonitorTell;
|
||||||
_enabled = _config.Enabled;
|
_enabled = _config.Enabled;
|
||||||
_debug = _config.Debug;
|
_debug = _config.Debug;
|
||||||
|
_autoAcceptEnabled = _config.AutoAcceptEnabled;
|
||||||
|
_autoAcceptFromFriends = _config.AutoAcceptFromFriends;
|
||||||
|
_autoAcceptFromFreeCompany = _config.AutoAcceptFromFreeCompany;
|
||||||
|
_autoAcceptScanAddons = _config.AutoAcceptScanAddons;
|
||||||
|
_cacheFriendsAndFcOnLogin = _config.CacheFriendsAndFcOnLogin;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Draw()
|
public override void Draw()
|
||||||
@@ -41,12 +53,18 @@ public sealed class ConfigWindow : Window
|
|||||||
{
|
{
|
||||||
_config.TriggerText = "inv";
|
_config.TriggerText = "inv";
|
||||||
_config.CaseSensitive = false;
|
_config.CaseSensitive = false;
|
||||||
|
_config.TriggerExactMatch = false;
|
||||||
_config.MonitorFreeCompany = true;
|
_config.MonitorFreeCompany = true;
|
||||||
_config.MonitorLinkShell = true;
|
_config.MonitorLinkShell = true;
|
||||||
_config.MonitorCrossWorldLinkShell = true;
|
_config.MonitorCrossWorldLinkShell = true;
|
||||||
_config.MonitorTell = true;
|
_config.MonitorTell = true;
|
||||||
_config.Enabled = true;
|
_config.Enabled = true;
|
||||||
_config.Debug = false;
|
_config.Debug = false;
|
||||||
|
_config.AutoAcceptEnabled = false;
|
||||||
|
_config.AutoAcceptFromFriends = true;
|
||||||
|
_config.AutoAcceptFromFreeCompany = true;
|
||||||
|
_config.AutoAcceptScanAddons = false;
|
||||||
|
_config.CacheFriendsAndFcOnLogin = true;
|
||||||
SyncFromConfig();
|
SyncFromConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,8 +77,10 @@ public sealed class ConfigWindow : Window
|
|||||||
if (ImGui.InputText("##trigger", ref _triggerTextInput, 128))
|
if (ImGui.InputText("##trigger", ref _triggerTextInput, 128))
|
||||||
_config.TriggerText = _triggerTextInput.Trim();
|
_config.TriggerText = _triggerTextInput.Trim();
|
||||||
|
|
||||||
ImGui.Checkbox("Case sensitive", ref _caseSensitive);
|
if (ImGui.Checkbox("Case sensitive", ref _caseSensitive))
|
||||||
_config.CaseSensitive = _caseSensitive;
|
_config.CaseSensitive = _caseSensitive;
|
||||||
|
if (ImGui.Checkbox("Exact match (whole message must equal trigger)", ref _triggerExactMatch))
|
||||||
|
_config.TriggerExactMatch = _triggerExactMatch;
|
||||||
|
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
ImGui.Text("Monitor these channels:");
|
ImGui.Text("Monitor these channels:");
|
||||||
@@ -78,5 +98,27 @@ public sealed class ConfigWindow : Window
|
|||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
if (ImGui.Checkbox("Debug logging (diagnose cross-world invite)", ref _debug))
|
if (ImGui.Checkbox("Debug logging (diagnose cross-world invite)", ref _debug))
|
||||||
_config.Debug = _debug;
|
_config.Debug = _debug;
|
||||||
|
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Text("Auto-accept party invites:");
|
||||||
|
ImGui.Indent();
|
||||||
|
if (ImGui.Checkbox("Auto-accept from friends and FC members", ref _autoAcceptEnabled))
|
||||||
|
_config.AutoAcceptEnabled = _autoAcceptEnabled;
|
||||||
|
if (_autoAcceptEnabled)
|
||||||
|
{
|
||||||
|
if (ImGui.Checkbox(" From friends", ref _autoAcceptFromFriends))
|
||||||
|
_config.AutoAcceptFromFriends = _autoAcceptFromFriends;
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ A [Dalamud](https://github.com/goatcorp/Dalamud) plugin for FFXIV (XIVLauncher)
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Configurable trigger text** – Set any word or phrase (e.g. `inv`, `invite`, `party`). When someone types it in a monitored channel, they are invited to your party.
|
- **Configurable trigger text** – Set any word or phrase (e.g. `inv`, `invite`, `party`). When someone types it in a monitored channel, they are invited to your party.
|
||||||
|
- **Auto-accept party invites** – Optionally automatically accept party invites when the inviter is on your friend list or is a member of your Free Company.
|
||||||
- **Channel selection** – Choose which channels to monitor:
|
- **Channel selection** – Choose which channels to monitor:
|
||||||
- Free Company
|
- Free Company
|
||||||
- Link Shell (1–8)
|
- Link Shell (1–8)
|
||||||
@@ -21,6 +22,10 @@ A [Dalamud](https://github.com/goatcorp/Dalamud) plugin for FFXIV (XIVLauncher)
|
|||||||
|
|
||||||
For **tells**, the game provides the sender’s content ID and world, so invites work reliably. For **FC, LS, and CWLS**, the plugin uses the sender’s content ID when available (e.g. from the last message in CWLS), so cross-world invites work for CWLS and same-world for FC/LS.
|
For **tells**, the game provides the sender’s content ID and world, so invites work reliably. For **FC, LS, and CWLS**, the plugin uses the sender’s content ID when available (e.g. from the last message in CWLS), so cross-world invites work for CWLS and same-world for FC/LS.
|
||||||
|
|
||||||
|
### Auto-accept party invites
|
||||||
|
|
||||||
|
When enabled, the plugin detects incoming party invites and automatically accepts them if the inviter is on your friend list or is a member of your Free Company. You can toggle friends and FC members independently. The friend list and FC roster must have been opened in-game at least once for the plugin to recognize members.
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
1. Open `HSRTools.sln` in Visual Studio or use the command line.
|
1. Open `HSRTools.sln` in Visual Studio or use the command line.
|
||||||
@@ -35,7 +40,7 @@ For **tells**, the game provides the sender’s content ID and world, so invites
|
|||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
- Open the plugin config from the Dalamud plugin list (right-click HSRTools → **Settings**).
|
- Open the plugin config from the Dalamud plugin list (right-click HSRTools → **Settings**).
|
||||||
- Set the trigger text, enable/disable the plugin, choose channels, and set case sensitivity.
|
- Set the trigger text, enable/disable the plugin, choose channels, set case sensitivity, and configure auto-accept (friends/FC).
|
||||||
- Settings are saved when you close the game or disable the plugin.
|
- Settings are saved when you close the game or disable the plugin.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|||||||
Reference in New Issue
Block a user