v1.0.5: Exact match trigger option; cross-world Friends for auto-accept

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-21 22:25:14 -05:00
parent 804a43ebf3
commit 58e7d2ee46
8 changed files with 49 additions and 6 deletions
+10
View File
@@ -4,6 +4,16 @@ 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 ## [1.0.4] - 2025-02-20
### Added ### Added
+7
View File
@@ -24,6 +24,13 @@ public class FriendFcCache
return Friends.Exists(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase) && p.WorldId == worldId); 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) public bool IsInFreeCompany(string name, ushort worldId)
{ {
if (FreeCompanyMembers == null || FreeCompanyMembers.Count == 0) return false; if (FreeCompanyMembers == null || FreeCompanyMembers.Count == 0) return false;
@@ -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>
+1 -1
View File
@@ -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.4.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>
+2 -1
View File
@@ -126,7 +126,7 @@ public sealed class AutoAcceptPartyService
if (_config.Debug) if (_config.Debug)
_log.Info($"[AutoAccept] Inviter: '{inviterName}' (worldId={inviterWorldId})"); _log.Info($"[AutoAccept] Inviter: '{inviterName}' (worldId={inviterWorldId})");
var isFriend = _config.AutoAcceptFromFriends && (IsFriendCached(inviterName, inviterWorldId) || IsFriend(inviterName, inviterWorldId)); var isFriend = _config.AutoAcceptFromFriends && (IsFriendCached(inviterName, inviterWorldId) || IsFriend(inviterName, inviterWorldId) || IsFriendCachedByName(inviterName));
var isFcMember = _config.AutoAcceptFromFreeCompany && (IsFreeCompanyMemberCached(inviterName, inviterWorldId) || IsFreeCompanyMember(inviterName, inviterWorldId)); var isFcMember = _config.AutoAcceptFromFreeCompany && (IsFreeCompanyMemberCached(inviterName, inviterWorldId) || IsFreeCompanyMember(inviterName, inviterWorldId));
if (_config.Debug) if (_config.Debug)
@@ -159,6 +159,7 @@ public sealed class AutoAcceptPartyService
} }
private bool IsFriendCached(string characterName, ushort worldId) => _cacheService.IsInFriends(characterName, worldId); 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 bool IsFreeCompanyMemberCached(string characterName, ushort worldId) => _cacheService.IsInFreeCompany(characterName, worldId);
private unsafe bool IsFriend(string characterName, ushort worldId) private unsafe bool IsFriend(string characterName, ushort worldId)
+10 -2
View File
@@ -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 (!messageText.Contains(trigger, comparison)) if (_config.TriggerExactMatch)
return; {
if (!messageText.Trim().Equals(trigger, comparison))
return;
}
else
{
if (!messageText.Contains(trigger, comparison))
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
@@ -86,6 +86,12 @@ public sealed class FriendFcCacheService
lock (_cacheLock) { return _cache.IsInFriends(name, 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) public bool IsInFreeCompany(string name, ushort worldId)
{ {
lock (_cacheLock) { return _cache.IsInFreeCompany(name, worldId); } lock (_cacheLock) { return _cache.IsInFreeCompany(name, worldId); }
+7 -2
View File
@@ -9,6 +9,7 @@ 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;
@@ -32,6 +33,7 @@ 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;
@@ -51,6 +53,7 @@ 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;
@@ -74,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:");