Initial release: HSRTools 1.0.0 - auto-invite on trigger word in FC/LS/CWLS/tells
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
using Dalamud.Plugin.Services;
|
||||
using HSRTools.Configuration;
|
||||
|
||||
namespace HSRTools.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Monitors chat for the user-defined trigger and initiates party invites.
|
||||
/// </summary>
|
||||
public sealed class ChatMonitorService
|
||||
{
|
||||
private readonly IChatGui _chatGui;
|
||||
private readonly PartyInviteService _partyInviteService;
|
||||
private readonly IPluginLog _log;
|
||||
private HSRToolsConfiguration _config;
|
||||
|
||||
private static readonly XivChatType[] LinkShellTypes =
|
||||
[
|
||||
XivChatType.Ls1, XivChatType.Ls2, XivChatType.Ls3, XivChatType.Ls4,
|
||||
XivChatType.Ls5, XivChatType.Ls6, XivChatType.Ls7, XivChatType.Ls8,
|
||||
];
|
||||
|
||||
private static readonly XivChatType[] CrossWorldLinkShellTypes =
|
||||
[
|
||||
XivChatType.CrossLinkShell1, XivChatType.CrossLinkShell2, XivChatType.CrossLinkShell3,
|
||||
XivChatType.CrossLinkShell4, XivChatType.CrossLinkShell5, XivChatType.CrossLinkShell6,
|
||||
XivChatType.CrossLinkShell7, XivChatType.CrossLinkShell8,
|
||||
];
|
||||
|
||||
public ChatMonitorService(
|
||||
IChatGui chatGui,
|
||||
PartyInviteService partyInviteService,
|
||||
IPluginLog log,
|
||||
HSRToolsConfiguration config)
|
||||
{
|
||||
_chatGui = chatGui;
|
||||
_partyInviteService = partyInviteService;
|
||||
_log = log;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public void SetConfiguration(HSRToolsConfiguration config)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_chatGui.ChatMessage += OnChatMessage;
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_chatGui.ChatMessage -= OnChatMessage;
|
||||
}
|
||||
|
||||
private void OnChatMessage(XivChatType type, int timestamp, ref SeString sender, ref SeString message, ref bool isHandled)
|
||||
{
|
||||
if (!_config.Enabled || string.IsNullOrWhiteSpace(_config.TriggerText))
|
||||
return;
|
||||
|
||||
if (!IsMonitoredChannel(type))
|
||||
return;
|
||||
|
||||
var messageText = message.TextValue;
|
||||
var trigger = _config.TriggerText;
|
||||
var comparison = _config.CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
|
||||
|
||||
if (!messageText.Contains(trigger, comparison))
|
||||
return;
|
||||
|
||||
// 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))
|
||||
{
|
||||
senderName = sender.TextValue.Trim();
|
||||
if (string.IsNullOrWhiteSpace(senderName))
|
||||
return;
|
||||
}
|
||||
|
||||
ulong contentId = 0;
|
||||
ushort worldId = _partyInviteService.GetLocalWorldId();
|
||||
|
||||
if (type == XivChatType.TellIncoming)
|
||||
{
|
||||
var (lastContentId, lastWorldId) = PartyInviteService.GetLastTellSenderInfo();
|
||||
if (lastContentId != 0)
|
||||
{
|
||||
contentId = lastContentId;
|
||||
worldId = lastWorldId;
|
||||
}
|
||||
}
|
||||
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)
|
||||
{
|
||||
contentId = logContentId;
|
||||
if (logWorldId != 0)
|
||||
worldId = logWorldId;
|
||||
}
|
||||
else if (worldIdFromPayload.HasValue && worldIdFromPayload.Value != 0)
|
||||
{
|
||||
// Fallback: use world from PlayerPayload (same-world works; cross-world may fail)
|
||||
worldId = worldIdFromPayload.Value;
|
||||
}
|
||||
}
|
||||
|
||||
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($"Trigger \"{trigger}\" detected from {senderName} in {type}. Sending party invite.");
|
||||
var success = _partyInviteService.TryInviteToParty(senderName, contentId, worldId);
|
||||
if (_config.Debug)
|
||||
_log.Info($"[HSRTools Debug] Invite result: {success}");
|
||||
if (!success)
|
||||
_log.Warning($"Failed to send party invite to {senderName}.");
|
||||
}
|
||||
|
||||
private bool IsMonitoredChannel(XivChatType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
XivChatType.FreeCompany => _config.MonitorFreeCompany,
|
||||
XivChatType.TellIncoming => _config.MonitorTell,
|
||||
_ when Array.IndexOf(LinkShellTypes, type) >= 0 => _config.MonitorLinkShell,
|
||||
_ when Array.IndexOf(CrossWorldLinkShellTypes, type) >= 0 => _config.MonitorCrossWorldLinkShell,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts player name and world ID from the sender's SeString when it contains a PlayerPayload
|
||||
/// (the clickable name link — world is stored in the payload, not in the visible text).
|
||||
/// Returns (name, worldId) when found; (null, null) otherwise.
|
||||
/// </summary>
|
||||
private static (string? Name, ushort? WorldId) TryGetSenderFromPayloads(SeString sender)
|
||||
{
|
||||
if (sender.Payloads == null)
|
||||
return (null, null);
|
||||
|
||||
foreach (var payload in sender.Payloads)
|
||||
{
|
||||
if (payload is PlayerPayload playerPayload)
|
||||
{
|
||||
var name = playerPayload.PlayerName?.Trim();
|
||||
if (string.IsNullOrEmpty(name))
|
||||
continue;
|
||||
var rowId = playerPayload.World.RowId;
|
||||
var worldId = rowId <= ushort.MaxValue ? (ushort)rowId : (ushort)0;
|
||||
return (name, worldId);
|
||||
}
|
||||
}
|
||||
return (null, null);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user