v1.0.1: Same-world + cross-world invites, suppress party-error toast
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,3 +1,6 @@
|
|||||||
|
# Release zip (build artifact, upload to GitHub Releases)
|
||||||
|
HSRTools.zip
|
||||||
|
|
||||||
# Build
|
# Build
|
||||||
[Bb]in/
|
[Bb]in/
|
||||||
[Oo]bj/
|
[Oo]bj/
|
||||||
|
|||||||
@@ -4,6 +4,18 @@ 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.1] - 2025-02-03
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Same-world invites (FC, LS, same-world CWLS) work again; cross-world CWLS invites work via ContentId.
|
||||||
|
- Same-world CWLS: fallback tries ContentId then name+world so invite succeeds; party-error toast is suppressed via IToastGui (ErrorToast/Toast) so the on-screen "Cannot locate a player with that name" no longer appears.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Log module is read at the start of the chat callback for CWLS so ContentId is captured reliably.
|
||||||
|
- FC/LS always use name+world (no ContentId). CWLS uses ContentId when available; same-world CWLS uses fallback (ContentId + name+world).
|
||||||
|
|
||||||
## [1.0.0] - 2025-02-02
|
## [1.0.0] - 2025-02-02
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -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.0.0</Version>
|
<Version>1.0.1.0</Version>
|
||||||
<Author>Knack117</Author>
|
<Author>Knack117</Author>
|
||||||
<Name>HSRTools</Name>
|
<Name>HSRTools</Name>
|
||||||
<InternalName>HSRTools</InternalName>
|
<InternalName>HSRTools</InternalName>
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ public sealed class ChatMonitorService
|
|||||||
private readonly IPluginLog _log;
|
private readonly IPluginLog _log;
|
||||||
private HSRToolsConfiguration _config;
|
private HSRToolsConfiguration _config;
|
||||||
|
|
||||||
|
/// <summary>When set, the next party-error message (e.g. from our fallback invite) will be suppressed.</summary>
|
||||||
|
private DateTime _suppressPartyErrorUntil = DateTime.MinValue;
|
||||||
|
|
||||||
|
private static readonly TimeSpan SuppressPartyErrorWindow = TimeSpan.FromSeconds(4);
|
||||||
|
|
||||||
private static readonly XivChatType[] LinkShellTypes =
|
private static readonly XivChatType[] LinkShellTypes =
|
||||||
[
|
[
|
||||||
XivChatType.Ls1, XivChatType.Ls2, XivChatType.Ls3, XivChatType.Ls4,
|
XivChatType.Ls1, XivChatType.Ls2, XivChatType.Ls3, XivChatType.Ls4,
|
||||||
@@ -51,15 +56,81 @@ public sealed class ChatMonitorService
|
|||||||
public void Start()
|
public void Start()
|
||||||
{
|
{
|
||||||
_chatGui.ChatMessage += OnChatMessage;
|
_chatGui.ChatMessage += OnChatMessage;
|
||||||
|
_chatGui.CheckMessageHandled += OnCheckMessageHandled;
|
||||||
|
PluginServices.ToastGui.Toast += OnToast;
|
||||||
|
PluginServices.ToastGui.ErrorToast += OnErrorToast;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Stop()
|
public void Stop()
|
||||||
{
|
{
|
||||||
|
PluginServices.ToastGui.ErrorToast -= OnErrorToast;
|
||||||
|
PluginServices.ToastGui.Toast -= OnToast;
|
||||||
_chatGui.ChatMessage -= OnChatMessage;
|
_chatGui.ChatMessage -= OnChatMessage;
|
||||||
|
_chatGui.CheckMessageHandled -= OnCheckMessageHandled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Suppress party-error toasts (big on-screen message) from our fallback invite. See BurntToast plugin.</summary>
|
||||||
|
private void OnErrorToast(ref SeString message, ref bool isHandled)
|
||||||
|
{
|
||||||
|
if (DateTime.UtcNow >= _suppressPartyErrorUntil || isHandled)
|
||||||
|
return;
|
||||||
|
var msg = message.TextValue;
|
||||||
|
var ord = StringComparison.OrdinalIgnoreCase;
|
||||||
|
if ((msg.Contains("locate", ord) && msg.Contains("that name", ord)) ||
|
||||||
|
msg.Contains("Unable to process party command", ord))
|
||||||
|
{
|
||||||
|
isHandled = true;
|
||||||
|
_suppressPartyErrorUntil = DateTime.MinValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnToast(ref SeString message, ref Dalamud.Game.Gui.Toast.ToastOptions options, ref bool isHandled)
|
||||||
|
{
|
||||||
|
if (DateTime.UtcNow >= _suppressPartyErrorUntil || isHandled)
|
||||||
|
return;
|
||||||
|
var msg = message.TextValue;
|
||||||
|
var ord = StringComparison.OrdinalIgnoreCase;
|
||||||
|
if ((msg.Contains("locate", ord) && msg.Contains("that name", ord)) ||
|
||||||
|
msg.Contains("Unable to process party command", ord))
|
||||||
|
{
|
||||||
|
isHandled = true;
|
||||||
|
_suppressPartyErrorUntil = DateTime.MinValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Suppress party-error messages in chat (CheckMessageHandled path).</summary>
|
||||||
|
private void OnCheckMessageHandled(XivChatType type, int timestamp, ref SeString sender, ref SeString message, ref bool isHandled)
|
||||||
|
{
|
||||||
|
if (DateTime.UtcNow >= _suppressPartyErrorUntil)
|
||||||
|
return;
|
||||||
|
var msg = message.TextValue;
|
||||||
|
if (msg.Length == 0)
|
||||||
|
return;
|
||||||
|
var ord = StringComparison.OrdinalIgnoreCase;
|
||||||
|
if ((msg.Contains("locate", ord) && msg.Contains("that name", ord)) ||
|
||||||
|
msg.Contains("Unable to process party command", ord))
|
||||||
|
{
|
||||||
|
isHandled = true;
|
||||||
|
_suppressPartyErrorUntil = DateTime.MinValue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnChatMessage(XivChatType type, int timestamp, ref SeString sender, ref SeString message, ref bool isHandled)
|
private void OnChatMessage(XivChatType type, int timestamp, ref SeString sender, ref SeString message, ref bool isHandled)
|
||||||
{
|
{
|
||||||
|
var msg = message.TextValue;
|
||||||
|
// Suppress party-error messages from our fallback invite (same-world CWLS tries ContentId then name+world; first can fail).
|
||||||
|
if (DateTime.UtcNow < _suppressPartyErrorUntil)
|
||||||
|
{
|
||||||
|
var ord = StringComparison.OrdinalIgnoreCase;
|
||||||
|
if ((msg.Contains("locate", ord) && msg.Contains("that name", ord)) ||
|
||||||
|
msg.Contains("Unable to process party command", ord))
|
||||||
|
{
|
||||||
|
isHandled = true;
|
||||||
|
_suppressPartyErrorUntil = DateTime.MinValue;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!_config.Enabled || string.IsNullOrWhiteSpace(_config.TriggerText))
|
if (!_config.Enabled || string.IsNullOrWhiteSpace(_config.TriggerText))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -73,6 +144,11 @@ public sealed class ChatMonitorService
|
|||||||
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).
|
||||||
|
var (logContentId, logWorldId) = type != XivChatType.TellIncoming
|
||||||
|
? PartyInviteService.GetCurrentChatSenderFromLogModule()
|
||||||
|
: (0UL, (ushort)0);
|
||||||
|
|
||||||
// Try to get name and world from the sender's link payload (the clickable name — world is in the payload, not visible text)
|
// Try to get name and world from the sender's link payload (the clickable name — world is in the payload, not visible text)
|
||||||
var (senderName, worldIdFromPayload) = TryGetSenderFromPayloads(sender);
|
var (senderName, worldIdFromPayload) = TryGetSenderFromPayloads(sender);
|
||||||
if (string.IsNullOrWhiteSpace(senderName))
|
if (string.IsNullOrWhiteSpace(senderName))
|
||||||
@@ -84,6 +160,7 @@ public sealed class ChatMonitorService
|
|||||||
|
|
||||||
ulong contentId = 0;
|
ulong contentId = 0;
|
||||||
ushort worldId = _partyInviteService.GetLocalWorldId();
|
ushort worldId = _partyInviteService.GetLocalWorldId();
|
||||||
|
var localWorldId = _partyInviteService.GetLocalWorldId();
|
||||||
|
|
||||||
if (type == XivChatType.TellIncoming)
|
if (type == XivChatType.TellIncoming)
|
||||||
{
|
{
|
||||||
@@ -96,31 +173,35 @@ public sealed class ChatMonitorService
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// For FC/LS/CWLS: server rejects name+world for cross-world ("Cannot locate character").
|
// FC/LS/CWLS: set worldId from payload (or local).
|
||||||
// Try to get ContentId from RaptureLogModule.AddonMessageSub3488 (filled when message is printed).
|
if (worldIdFromPayload.HasValue && worldIdFromPayload.Value != 0)
|
||||||
var (logContentId, logWorldId) = PartyInviteService.GetCurrentChatSenderFromLogModule();
|
worldId = worldIdFromPayload.Value;
|
||||||
if (logContentId != 0)
|
|
||||||
|
var isFcOrLs = type == XivChatType.FreeCompany || Array.IndexOf(LinkShellTypes, type) >= 0;
|
||||||
|
var isCwls = Array.IndexOf(CrossWorldLinkShellTypes, type) >= 0;
|
||||||
|
|
||||||
|
if (isCwls && logContentId != 0)
|
||||||
{
|
{
|
||||||
contentId = logContentId;
|
contentId = logContentId;
|
||||||
if (logWorldId != 0)
|
if (logWorldId != 0)
|
||||||
worldId = logWorldId;
|
worldId = logWorldId;
|
||||||
}
|
}
|
||||||
else if (worldIdFromPayload.HasValue && worldIdFromPayload.Value != 0)
|
else if (isFcOrLs)
|
||||||
{
|
{
|
||||||
// Fallback: use world from PlayerPayload (same-world works; cross-world may fail)
|
contentId = 0;
|
||||||
worldId = worldIdFromPayload.Value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_config.Debug)
|
if (_config.Debug)
|
||||||
{
|
{
|
||||||
var payloadInfo = sender.Payloads == null
|
_log.Info($"[HSRTools Debug] {type} name='{senderName}' worldId={worldId} localWorldId={localWorldId} contentId={contentId} logContentId={logContentId} (0=name+world, non-zero=ContentId)");
|
||||||
? "null"
|
|
||||||
: string.Join(", ", sender.Payloads.Select(p => p.Type.ToString()));
|
|
||||||
_log.Info($"[HSRTools Debug] Sender payloads ({sender.Payloads?.Count ?? 0}): [{payloadInfo}]. " +
|
|
||||||
$"Resolved name='{senderName}' worldIdFromPayload={worldIdFromPayload?.ToString() ?? "null"} usingWorldId={worldId} contentId={contentId} (0=name+world invite, non-zero=ContentId invite)");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Same-world CWLS with contentId uses fallback (ContentId then name+world); first attempt can show an error — suppress it.
|
||||||
|
var isSameWorldCwls = Array.IndexOf(CrossWorldLinkShellTypes, type) >= 0 && contentId != 0 && localWorldId != 0 && worldId == localWorldId;
|
||||||
|
if (isSameWorldCwls)
|
||||||
|
_suppressPartyErrorUntil = DateTime.UtcNow.Add(SuppressPartyErrorWindow);
|
||||||
|
|
||||||
_log.Info($"Trigger \"{trigger}\" detected from {senderName} in {type}. Sending party invite.");
|
_log.Info($"Trigger \"{trigger}\" detected from {senderName} in {type}. Sending party invite.");
|
||||||
var success = _partyInviteService.TryInviteToParty(senderName, contentId, worldId);
|
var success = _partyInviteService.TryInviteToParty(senderName, contentId, worldId);
|
||||||
if (_config.Debug)
|
if (_config.Debug)
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ public sealed class PartyInviteService
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Attempts to invite a player to the party.
|
/// Attempts to invite a player to the party.
|
||||||
/// When contentId is non-zero, uses InviteToPartyContentId (required for cross-world; name+world is rejected by server).
|
/// Same-world: use InviteToParty(contentId, name, worldId) with actual contentId; InviteToPartyContentId causes "Unable to process Party Command".
|
||||||
/// Otherwise uses InviteToParty(name, 0, worldId) for same-world.
|
/// Cross-world: use InviteToPartyContentId(contentId, worldId); name+world returns "Cannot locate character".
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool TryInviteToParty(string senderName, ulong contentId, ushort worldId)
|
public bool TryInviteToParty(string senderName, ulong contentId, ushort worldId)
|
||||||
{
|
{
|
||||||
@@ -71,15 +71,24 @@ public sealed class PartyInviteService
|
|||||||
if (partyInviteProxy == null)
|
if (partyInviteProxy == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
var localWorldId = GetLocalWorldId();
|
||||||
|
var isSameWorld = localWorldId != 0 && worldId == localWorldId;
|
||||||
|
|
||||||
if (contentId != 0)
|
if (contentId != 0)
|
||||||
{
|
{
|
||||||
// Cross-world: server requires ContentId; name+world returns "Cannot locate character"
|
if (isSameWorld)
|
||||||
|
{
|
||||||
|
// Same-world CWLS: try both; the game may accept one. ContentId first, then name+world fallback.
|
||||||
|
_ = partyInviteProxy->InviteToPartyContentId(contentId, worldId);
|
||||||
|
if (!string.IsNullOrWhiteSpace(senderName))
|
||||||
|
return partyInviteProxy->InviteToParty(0, senderName, worldId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return partyInviteProxy->InviteToPartyContentId(contentId, worldId);
|
return partyInviteProxy->InviteToPartyContentId(contentId, worldId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(senderName))
|
if (string.IsNullOrWhiteSpace(senderName))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return partyInviteProxy->InviteToParty(0, senderName, worldId);
|
return partyInviteProxy->InviteToParty(0, senderName, worldId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,4 +11,5 @@ public sealed class PluginServices
|
|||||||
[PluginService] public static IPlayerState PlayerState { get; private set; } = null!;
|
[PluginService] public static IPlayerState PlayerState { get; private set; } = null!;
|
||||||
[PluginService] public static IPluginLog PluginLog { get; private set; } = null!;
|
[PluginService] public static IPluginLog PluginLog { get; private set; } = null!;
|
||||||
[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!;
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -5,15 +5,15 @@
|
|||||||
"Punchline": "Auto-invite to party when a trigger word is said in FC, LS, CWLS, or tells.",
|
"Punchline": "Auto-invite to party when a trigger word is said in FC, LS, CWLS, or tells.",
|
||||||
"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 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).",
|
||||||
"InternalName": "HSRTools",
|
"InternalName": "HSRTools",
|
||||||
"AssemblyVersion": "1.0.0.0",
|
"AssemblyVersion": "1.0.1.0",
|
||||||
"RepoUrl": "https://github.com/Knack117/HSRTools",
|
"RepoUrl": "https://github.com/Knack117/HSRTools",
|
||||||
"ApplicableVersion": "any",
|
"ApplicableVersion": "any",
|
||||||
"DalamudApiLevel": 14,
|
"DalamudApiLevel": 14,
|
||||||
"Tags": [ "chat", "party", "invite", "automation" ],
|
"Tags": [ "chat", "party", "invite", "automation" ],
|
||||||
"AcceptsFeedback": true,
|
"AcceptsFeedback": true,
|
||||||
"DownloadCount": 0,
|
"DownloadCount": 0,
|
||||||
"LastUpdate": "1738454400",
|
"LastUpdate": "1738627200",
|
||||||
"DownloadLinkInstall": "https://github.com/Knack117/HSRTools/releases/download/v1.0.0/HSRTools.zip",
|
"DownloadLinkInstall": "https://github.com/Knack117/HSRTools/releases/download/v1.0.1/HSRTools.zip",
|
||||||
"DownloadLinkUpdate": "https://github.com/Knack117/HSRTools/releases/download/v1.0.0/HSRTools.zip"
|
"DownloadLinkUpdate": "https://github.com/Knack117/HSRTools/releases/download/v1.0.1/HSRTools.zip"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user