diff --git a/ChatTwo/AutoTellTabsService.cs b/ChatTwo/AutoTellTabsService.cs index 06454bf..497ca57 100644 --- a/ChatTwo/AutoTellTabsService.cs +++ b/ChatTwo/AutoTellTabsService.cs @@ -82,8 +82,14 @@ internal sealed class AutoTellTabsService : IDisposable if (partner == null) { // Real message without a player payload — e.g. GM tells, which - // we deliberately skip. Warn once so we notice future regressions. - Plugin.Log.Warning("[AutoTellTabs] Could not extract tell partner from message; skipping spawn."); + // we deliberately skip. The diagnostics make future regressions + // (FFXIV changing tell payload shape, new edge cases) findable + // without having to crank up debug logging at the source. + Plugin.Log.Warning( + $"[AutoTellTabs] Could not extract tell partner. type={message.Code.Type}, " + + $"senderChunks={message.Sender.Count}, contentChunks={message.Content.Count}, " + + $"senderSourcePayloads={message.SenderSource?.Payloads?.Count ?? 0}, " + + $"contentSourcePayloads={message.ContentSource?.Payloads?.Count ?? 0}"); return; } @@ -111,8 +117,12 @@ internal sealed class AutoTellTabsService : IDisposable { if (message.Code.Type == ChatType.TellIncoming) { - // Incoming tell: the sender is the conversation partner. - var fromSender = ChunkUtil.TryGetPlayerPayload(message.Sender); + // Incoming tell: the sender is the conversation partner. The + // PlayerPayload normally rides on a chunk's Link slot, but for + // some tell types FFXIV only puts it in the raw SeString — + // fall back to that before giving up. + var fromSender = ChunkUtil.TryGetPlayerPayload(message.Sender) + ?? ChunkUtil.TryGetPlayerPayload(message.SenderSource); if (fromSender != null) { return (fromSender.PlayerName, fromSender.World.RowId); @@ -123,8 +133,11 @@ internal sealed class AutoTellTabsService : IDisposable // Outgoing tell: the local player is the sender, the partner shows // up either as a payload in the content (for tells typed via the // Chat 2 input bar) or as the channel's tracked tell target (set by - // the SetContextTellTarget game hook). - var fromContent = ChunkUtil.TryGetPlayerPayload(message.Content); + // the SetContextTellTarget game hook). Same SeString fallback. + var fromContent = ChunkUtil.TryGetPlayerPayload(message.Content) + ?? ChunkUtil.TryGetPlayerPayload(message.ContentSource) + ?? ChunkUtil.TryGetPlayerPayload(message.Sender) + ?? ChunkUtil.TryGetPlayerPayload(message.SenderSource); if (fromContent != null) { return (fromContent.PlayerName, fromContent.World.RowId); diff --git a/ChatTwo/Util/ChunkUtil.cs b/ChatTwo/Util/ChunkUtil.cs index 48702e7..8077ac7 100755 --- a/ChatTwo/Util/ChunkUtil.cs +++ b/ChatTwo/Util/ChunkUtil.cs @@ -415,6 +415,26 @@ internal static class ChunkUtil return null; } + // Fallback for tells where the PlayerPayload lives in the raw SeString + // payload list rather than on a chunk's Link slot. Same semantics as + // the chunk-walking variant above: returns the first PlayerPayload or + // null if the SeString has none. + internal static PlayerPayload? TryGetPlayerPayload(SeString? seString) + { + if (seString == null) + { + return null; + } + foreach (var payload in seString.Payloads) + { + if (payload is PlayerPayload pp) + { + return pp; + } + } + return null; + } + // True when the message's sender (or, as a fallback, content) carries a // PlayerPayload that matches the given identity. Used by both the // Tab.Matches sender filter and the MessageStore tell-history scan.