diff --git a/.gitignore b/.gitignore index f28b7b3..287dd75 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Release zip (build artifact, upload to GitHub Releases) +HSRTools.zip + # Build [Bb]in/ [Oo]bj/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b2498e..ba423f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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/). +## [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 ### Added diff --git a/HSRTools/HSRTools.csproj b/HSRTools/HSRTools.csproj index 1574479..56e0786 100644 --- a/HSRTools/HSRTools.csproj +++ b/HSRTools/HSRTools.csproj @@ -1,7 +1,7 @@ true - 1.0.0.0 + 1.0.1.0 Knack117 HSRTools HSRTools diff --git a/HSRTools/Services/ChatMonitorService.cs b/HSRTools/Services/ChatMonitorService.cs index e0ebbfe..224339d 100644 --- a/HSRTools/Services/ChatMonitorService.cs +++ b/HSRTools/Services/ChatMonitorService.cs @@ -18,6 +18,11 @@ public sealed class ChatMonitorService private readonly IPluginLog _log; private HSRToolsConfiguration _config; + /// When set, the next party-error message (e.g. from our fallback invite) will be suppressed. + private DateTime _suppressPartyErrorUntil = DateTime.MinValue; + + private static readonly TimeSpan SuppressPartyErrorWindow = TimeSpan.FromSeconds(4); + private static readonly XivChatType[] LinkShellTypes = [ XivChatType.Ls1, XivChatType.Ls2, XivChatType.Ls3, XivChatType.Ls4, @@ -51,15 +56,81 @@ public sealed class ChatMonitorService public void Start() { _chatGui.ChatMessage += OnChatMessage; + _chatGui.CheckMessageHandled += OnCheckMessageHandled; + PluginServices.ToastGui.Toast += OnToast; + PluginServices.ToastGui.ErrorToast += OnErrorToast; } public void Stop() { + PluginServices.ToastGui.ErrorToast -= OnErrorToast; + PluginServices.ToastGui.Toast -= OnToast; _chatGui.ChatMessage -= OnChatMessage; + _chatGui.CheckMessageHandled -= OnCheckMessageHandled; + } + + /// Suppress party-error toasts (big on-screen message) from our fallback invite. See BurntToast plugin. + 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; + } + } + + /// Suppress party-error messages in chat (CheckMessageHandled path). + 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) { + 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)) return; @@ -73,6 +144,11 @@ public sealed class ChatMonitorService if (!messageText.Contains(trigger, comparison)) 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) var (senderName, worldIdFromPayload) = TryGetSenderFromPayloads(sender); if (string.IsNullOrWhiteSpace(senderName)) @@ -84,6 +160,7 @@ public sealed class ChatMonitorService ulong contentId = 0; ushort worldId = _partyInviteService.GetLocalWorldId(); + var localWorldId = _partyInviteService.GetLocalWorldId(); if (type == XivChatType.TellIncoming) { @@ -96,31 +173,35 @@ public sealed class ChatMonitorService } else { - // For FC/LS/CWLS: server rejects name+world for cross-world ("Cannot locate character"). - // Try to get ContentId from RaptureLogModule.AddonMessageSub3488 (filled when message is printed). - var (logContentId, logWorldId) = PartyInviteService.GetCurrentChatSenderFromLogModule(); - if (logContentId != 0) + // FC/LS/CWLS: set worldId from payload (or local). + if (worldIdFromPayload.HasValue && worldIdFromPayload.Value != 0) + worldId = worldIdFromPayload.Value; + + var isFcOrLs = type == XivChatType.FreeCompany || Array.IndexOf(LinkShellTypes, type) >= 0; + var isCwls = Array.IndexOf(CrossWorldLinkShellTypes, type) >= 0; + + if (isCwls && logContentId != 0) { contentId = logContentId; if (logWorldId != 0) worldId = logWorldId; } - else if (worldIdFromPayload.HasValue && worldIdFromPayload.Value != 0) + else if (isFcOrLs) { - // Fallback: use world from PlayerPayload (same-world works; cross-world may fail) - worldId = worldIdFromPayload.Value; + contentId = 0; } } if (_config.Debug) { - var payloadInfo = sender.Payloads == null - ? "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)"); + _log.Info($"[HSRTools Debug] {type} name='{senderName}' worldId={worldId} localWorldId={localWorldId} contentId={contentId} logContentId={logContentId} (0=name+world, non-zero=ContentId)"); } + // 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."); var success = _partyInviteService.TryInviteToParty(senderName, contentId, worldId); if (_config.Debug) diff --git a/HSRTools/Services/PartyInviteService.cs b/HSRTools/Services/PartyInviteService.cs index 7d3b2aa..893fef0 100644 --- a/HSRTools/Services/PartyInviteService.cs +++ b/HSRTools/Services/PartyInviteService.cs @@ -56,8 +56,8 @@ public sealed class PartyInviteService /// /// 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). - /// Otherwise uses InviteToParty(name, 0, worldId) for same-world. + /// Same-world: use InviteToParty(contentId, name, worldId) with actual contentId; InviteToPartyContentId causes "Unable to process Party Command". + /// Cross-world: use InviteToPartyContentId(contentId, worldId); name+world returns "Cannot locate character". /// public bool TryInviteToParty(string senderName, ulong contentId, ushort worldId) { @@ -71,15 +71,24 @@ public sealed class PartyInviteService if (partyInviteProxy == null) return false; + var localWorldId = GetLocalWorldId(); + var isSameWorld = localWorldId != 0 && worldId == localWorldId; + 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); } if (string.IsNullOrWhiteSpace(senderName)) return false; - return partyInviteProxy->InviteToParty(0, senderName, worldId); } } diff --git a/HSRTools/Services/PluginServices.cs b/HSRTools/Services/PluginServices.cs index 231807e..584a7aa 100644 --- a/HSRTools/Services/PluginServices.cs +++ b/HSRTools/Services/PluginServices.cs @@ -11,4 +11,5 @@ public sealed class PluginServices [PluginService] public static IPlayerState PlayerState { 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 IToastGui ToastGui { get; private set; } = null!; } diff --git a/pluginmaster.json b/pluginmaster.json index e7e81b3..9283378 100644 --- a/pluginmaster.json +++ b/pluginmaster.json @@ -5,15 +5,15 @@ "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).", "InternalName": "HSRTools", - "AssemblyVersion": "1.0.0.0", + "AssemblyVersion": "1.0.1.0", "RepoUrl": "https://github.com/Knack117/HSRTools", "ApplicableVersion": "any", "DalamudApiLevel": 14, "Tags": [ "chat", "party", "invite", "automation" ], "AcceptsFeedback": true, "DownloadCount": 0, - "LastUpdate": "1738454400", - "DownloadLinkInstall": "https://github.com/Knack117/HSRTools/releases/download/v1.0.0/HSRTools.zip", - "DownloadLinkUpdate": "https://github.com/Knack117/HSRTools/releases/download/v1.0.0/HSRTools.zip" + "LastUpdate": "1738627200", + "DownloadLinkInstall": "https://github.com/Knack117/HSRTools/releases/download/v1.0.1/HSRTools.zip", + "DownloadLinkUpdate": "https://github.com/Knack117/HSRTools/releases/download/v1.0.1/HSRTools.zip" } ]