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:
+71
@@ -0,0 +1,71 @@
|
|||||||
|
# Build
|
||||||
|
[Bb]in/
|
||||||
|
[Oo]bj/
|
||||||
|
[Dd]ebug/
|
||||||
|
[Rr]elease/
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
[Aa][Rr][Mm]/
|
||||||
|
[Aa][Rr][Mm]64/
|
||||||
|
bld/
|
||||||
|
[Bb]uild/
|
||||||
|
[Oo]ut/
|
||||||
|
[Ll]og/
|
||||||
|
[Ll]ogs/
|
||||||
|
|
||||||
|
# Visual Studio
|
||||||
|
.vs/
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.suo
|
||||||
|
*.cache
|
||||||
|
*.ilk
|
||||||
|
*.log
|
||||||
|
*.meta
|
||||||
|
*.obj
|
||||||
|
*.pch
|
||||||
|
*.pdb
|
||||||
|
*.pgc
|
||||||
|
*.pgd
|
||||||
|
*.rsp
|
||||||
|
*.sbr
|
||||||
|
*.tlb
|
||||||
|
*.tli
|
||||||
|
*.tlh
|
||||||
|
*.tmp
|
||||||
|
*.tmp_proj
|
||||||
|
*.vspscc
|
||||||
|
*.vssscc
|
||||||
|
.builds
|
||||||
|
*.pidb
|
||||||
|
*.svclog
|
||||||
|
*.scc
|
||||||
|
|
||||||
|
# NuGet
|
||||||
|
packages/
|
||||||
|
*.nupkg
|
||||||
|
*.snupkg
|
||||||
|
**/packages/*
|
||||||
|
!**/packages/build/
|
||||||
|
project.lock.json
|
||||||
|
project.fragment.lock.json
|
||||||
|
artifacts/
|
||||||
|
|
||||||
|
# Rider
|
||||||
|
.idea/
|
||||||
|
*.sln.iml
|
||||||
|
|
||||||
|
# User-specific
|
||||||
|
*.rsuser
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
Thumbs.db
|
||||||
|
ehthumbs.db
|
||||||
|
Desktop.ini
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
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.0] - 2025-02-02
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Initial release.
|
||||||
|
- Trigger-word detection in Free Company, Link Shell (1–8), Cross-World Link Shell (1–8), and tells.
|
||||||
|
- Auto-invite to party when a monitored message contains the configured trigger text.
|
||||||
|
- Configurable trigger text (default: `inv`), case sensitivity, and channel toggles.
|
||||||
|
- Cross-world invite support for tells and CWLS (via ContentId when available).
|
||||||
|
- Debug option to log invite flow for troubleshooting.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.0.31903.59
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HSRTools", "HSRTools\HSRTools.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
namespace HSRTools.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration for the HSRTools plugin.
|
||||||
|
/// </summary>
|
||||||
|
public class HSRToolsConfiguration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The trigger word or text that, when seen in chat, will invite the sender to party.
|
||||||
|
/// </summary>
|
||||||
|
public string TriggerText { get; set; } = "inv";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the trigger match is case-sensitive.
|
||||||
|
/// </summary>
|
||||||
|
public bool CaseSensitive { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to monitor Free Company chat.
|
||||||
|
/// </summary>
|
||||||
|
public bool MonitorFreeCompany { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to monitor Link Shell 1-8 chat.
|
||||||
|
/// </summary>
|
||||||
|
public bool MonitorLinkShell { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to monitor Cross-World Link Shell 1-8 chat.
|
||||||
|
/// </summary>
|
||||||
|
public bool MonitorCrossWorldLinkShell { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to monitor incoming tells/whispers.
|
||||||
|
/// </summary>
|
||||||
|
public bool MonitorTell { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the plugin is enabled.
|
||||||
|
/// </summary>
|
||||||
|
public bool Enabled { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When true, log detailed info when a trigger fires (sender payloads, world id, invite result).
|
||||||
|
/// Use this to diagnose cross-world invite issues.
|
||||||
|
/// </summary>
|
||||||
|
public bool Debug { get; set; } = false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Dalamud.NET.Sdk/14.0.1">
|
||||||
|
<PropertyGroup>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
<Version>1.0.0.0</Version>
|
||||||
|
<Author>Knack117</Author>
|
||||||
|
<Name>HSRTools</Name>
|
||||||
|
<InternalName>HSRTools</InternalName>
|
||||||
|
<Punchline>Auto-invite to party when a trigger word is said in FC, LS, CWLS, or tells.</Punchline>
|
||||||
|
<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>
|
||||||
|
<RepoUrl>https://github.com/Knack117/HSRTools</RepoUrl>
|
||||||
|
<Tags>chat, party, invite, automation</Tags>
|
||||||
|
<AcceptsFeedback>true</AcceptsFeedback>
|
||||||
|
</PropertyGroup>
|
||||||
|
<!-- FFXIVClientStructs is provided by Dalamud.NET.Sdk when building for the game. -->
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"Author": "Knack117",
|
||||||
|
"Name": "HSRTools",
|
||||||
|
"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).",
|
||||||
|
"RepoUrl": "https://github.com/Knack117/HSRTools",
|
||||||
|
"Tags": [ "chat", "party", "invite", "automation" ],
|
||||||
|
"AcceptsFeedback": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Dalamud.Interface.Windowing;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
using HSRTools.Configuration;
|
||||||
|
using HSRTools.Services;
|
||||||
|
using HSRTools.UI;
|
||||||
|
|
||||||
|
namespace HSRTools;
|
||||||
|
|
||||||
|
public sealed class HSRToolsPlugin : IDalamudPlugin
|
||||||
|
{
|
||||||
|
private const string ConfigFileName = "HSRTools.json";
|
||||||
|
|
||||||
|
private readonly string _configDir;
|
||||||
|
private readonly ChatMonitorService _chatMonitorService;
|
||||||
|
private readonly ConfigWindow _configWindow;
|
||||||
|
private readonly WindowSystem _windowSystem;
|
||||||
|
private HSRToolsConfiguration _config;
|
||||||
|
|
||||||
|
public HSRToolsPlugin(IDalamudPluginInterface pluginInterface)
|
||||||
|
{
|
||||||
|
pluginInterface.Create<PluginServices>();
|
||||||
|
|
||||||
|
_configDir = PluginServices.PluginInterface.ConfigDirectory.FullName;
|
||||||
|
_config = LoadConfig(_configDir) ?? new HSRToolsConfiguration();
|
||||||
|
|
||||||
|
_chatMonitorService = new ChatMonitorService(
|
||||||
|
PluginServices.ChatGui,
|
||||||
|
new PartyInviteService(PluginServices.DataManager, PluginServices.PlayerState),
|
||||||
|
PluginServices.PluginLog,
|
||||||
|
_config);
|
||||||
|
|
||||||
|
_configWindow = new ConfigWindow(_config);
|
||||||
|
_windowSystem = new WindowSystem("HSRTools");
|
||||||
|
_windowSystem.AddWindow(_configWindow);
|
||||||
|
|
||||||
|
PluginServices.PluginInterface.UiBuilder.Draw += OnDraw;
|
||||||
|
PluginServices.PluginInterface.UiBuilder.OpenConfigUi += OpenConfigUi;
|
||||||
|
_chatMonitorService.SetConfiguration(_config);
|
||||||
|
_chatMonitorService.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_chatMonitorService.Stop();
|
||||||
|
_windowSystem.RemoveAllWindows();
|
||||||
|
SaveConfig(_configDir, _config);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDraw()
|
||||||
|
{
|
||||||
|
_windowSystem.Draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenConfigUi()
|
||||||
|
{
|
||||||
|
_configWindow.IsOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HSRToolsConfiguration? LoadConfig(string configDir)
|
||||||
|
{
|
||||||
|
var path = Path.Combine(configDir, ConfigFileName);
|
||||||
|
if (!File.Exists(path))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(path);
|
||||||
|
return JsonSerializer.Deserialize<HSRToolsConfiguration>(json);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SaveConfig(string configDir, HSRToolsConfiguration config)
|
||||||
|
{
|
||||||
|
var path = Path.Combine(configDir, ConfigFileName);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(configDir);
|
||||||
|
var json = JsonSerializer.Serialize(config, new JsonSerializerOptions { WriteIndented = true });
|
||||||
|
File.WriteAllText(path, json);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
using System;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI.Info;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI.Shell;
|
||||||
|
using Lumina.Excel.Sheets;
|
||||||
|
|
||||||
|
namespace HSRTools.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service that performs party invites using the game's InfoProxyPartyInvite.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PartyInviteService
|
||||||
|
{
|
||||||
|
private readonly IDataManager _dataManager;
|
||||||
|
private readonly IPlayerState _playerState;
|
||||||
|
|
||||||
|
public PartyInviteService(IDataManager dataManager, IPlayerState playerState)
|
||||||
|
{
|
||||||
|
_dataManager = dataManager;
|
||||||
|
_playerState = playerState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolves a world name (e.g. from "Name@Gilgamesh") to the game's world ID.
|
||||||
|
/// Returns 0 if not found.
|
||||||
|
/// </summary>
|
||||||
|
public ushort GetWorldIdByName(string worldName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(worldName))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var sheet = _dataManager.GetExcelSheet<World>();
|
||||||
|
if (sheet == null)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var trimmed = worldName.Trim();
|
||||||
|
foreach (var row in sheet)
|
||||||
|
{
|
||||||
|
if (string.Equals(row.Name.ExtractText(), trimmed, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var rowId = row.RowId;
|
||||||
|
return rowId <= ushort.MaxValue ? (ushort)rowId : (ushort)0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public bool TryInviteToParty(string senderName, ulong contentId, ushort worldId)
|
||||||
|
{
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
var infoModule = InfoModule.Instance();
|
||||||
|
if (infoModule == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var partyInviteProxy = (InfoProxyPartyInvite*)infoModule->GetInfoProxyById(InfoProxyId.PartyInvite);
|
||||||
|
if (partyInviteProxy == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (contentId != 0)
|
||||||
|
{
|
||||||
|
// Cross-world: server requires ContentId; name+world returns "Cannot locate character"
|
||||||
|
return partyInviteProxy->InviteToPartyContentId(contentId, worldId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(senderName))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return partyInviteProxy->InviteToParty(0, senderName, worldId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current chat message sender's ContentId and WorldId from RaptureLogModule.AddonMessageSub3488.
|
||||||
|
/// The game fills this when processing a chat message (e.g. right before/during PrintMessage).
|
||||||
|
/// Call this from the ChatMessage handler to get the sender's ContentId for cross-world invite.
|
||||||
|
/// Returns (0, 0) if not available.
|
||||||
|
/// </summary>
|
||||||
|
public static (ulong ContentId, ushort WorldId) GetCurrentChatSenderFromLogModule()
|
||||||
|
{
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
var logModule = RaptureLogModule.Instance();
|
||||||
|
if (logModule == null)
|
||||||
|
return (0, 0);
|
||||||
|
|
||||||
|
ref var sub = ref logModule->AddonMessageSub3488;
|
||||||
|
if (sub.ContentId == 0)
|
||||||
|
return (0, 0);
|
||||||
|
|
||||||
|
return (sub.ContentId, sub.WorldId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the local player's current world ID for use when inviting same-world players.
|
||||||
|
/// </summary>
|
||||||
|
public ushort GetLocalWorldId()
|
||||||
|
{
|
||||||
|
if (!_playerState.IsLoaded)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var rowId = _playerState.CurrentWorld.RowId;
|
||||||
|
return rowId <= ushort.MaxValue ? (ushort)rowId : (ushort)0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// For TellIncoming, the game stores the sender's content ID and world in RaptureShellModule.
|
||||||
|
/// Call this when you just received a tell to get that sender's info.
|
||||||
|
/// </summary>
|
||||||
|
public static (ulong ContentId, ushort WorldId) GetLastTellSenderInfo()
|
||||||
|
{
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
var shell = RaptureShellModule.Instance();
|
||||||
|
if (shell == null)
|
||||||
|
return (0, 0);
|
||||||
|
|
||||||
|
return (shell->ContentId, shell->TellWorldId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using Dalamud.IoC;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
|
||||||
|
namespace HSRTools.Services;
|
||||||
|
|
||||||
|
public sealed class PluginServices
|
||||||
|
{
|
||||||
|
[PluginService] public static IDataManager DataManager { get; private set; } = null!;
|
||||||
|
[PluginService] public static IChatGui ChatGui { 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 IDalamudPluginInterface PluginInterface { get; private set; } = null!;
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using Dalamud.Interface.Windowing;
|
||||||
|
using HSRTools.Configuration;
|
||||||
|
|
||||||
|
namespace HSRTools.UI;
|
||||||
|
|
||||||
|
public sealed class ConfigWindow : Window
|
||||||
|
{
|
||||||
|
private readonly HSRToolsConfiguration _config;
|
||||||
|
private string _triggerTextInput = string.Empty;
|
||||||
|
private bool _caseSensitive;
|
||||||
|
private bool _monitorFreeCompany;
|
||||||
|
private bool _monitorLinkShell;
|
||||||
|
private bool _monitorCrossWorldLinkShell;
|
||||||
|
private bool _monitorTell;
|
||||||
|
private bool _enabled;
|
||||||
|
private bool _debug;
|
||||||
|
|
||||||
|
public ConfigWindow(HSRToolsConfiguration config)
|
||||||
|
: base("HSRTools Configuration", ImGuiWindowFlags.AlwaysAutoResize)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
SyncFromConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SyncFromConfig()
|
||||||
|
{
|
||||||
|
_triggerTextInput = _config.TriggerText ?? string.Empty;
|
||||||
|
_caseSensitive = _config.CaseSensitive;
|
||||||
|
_monitorFreeCompany = _config.MonitorFreeCompany;
|
||||||
|
_monitorLinkShell = _config.MonitorLinkShell;
|
||||||
|
_monitorCrossWorldLinkShell = _config.MonitorCrossWorldLinkShell;
|
||||||
|
_monitorTell = _config.MonitorTell;
|
||||||
|
_enabled = _config.Enabled;
|
||||||
|
_debug = _config.Debug;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Draw()
|
||||||
|
{
|
||||||
|
if (ImGui.Button("Reset to defaults"))
|
||||||
|
{
|
||||||
|
_config.TriggerText = "inv";
|
||||||
|
_config.CaseSensitive = false;
|
||||||
|
_config.MonitorFreeCompany = true;
|
||||||
|
_config.MonitorLinkShell = true;
|
||||||
|
_config.MonitorCrossWorldLinkShell = true;
|
||||||
|
_config.MonitorTell = true;
|
||||||
|
_config.Enabled = true;
|
||||||
|
_config.Debug = false;
|
||||||
|
SyncFromConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Checkbox("Plugin enabled", ref _enabled);
|
||||||
|
_config.Enabled = _enabled;
|
||||||
|
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Text("Trigger text (word or phrase that triggers an invite when seen in chat):");
|
||||||
|
if (ImGui.InputText("##trigger", ref _triggerTextInput, 128))
|
||||||
|
_config.TriggerText = _triggerTextInput.Trim();
|
||||||
|
|
||||||
|
ImGui.Checkbox("Case sensitive", ref _caseSensitive);
|
||||||
|
_config.CaseSensitive = _caseSensitive;
|
||||||
|
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Text("Monitor these channels:");
|
||||||
|
ImGui.Indent();
|
||||||
|
if (ImGui.Checkbox("Free Company", ref _monitorFreeCompany))
|
||||||
|
_config.MonitorFreeCompany = _monitorFreeCompany;
|
||||||
|
if (ImGui.Checkbox("Link Shell (1-8)", ref _monitorLinkShell))
|
||||||
|
_config.MonitorLinkShell = _monitorLinkShell;
|
||||||
|
if (ImGui.Checkbox("Cross-World Link Shell (1-8)", ref _monitorCrossWorldLinkShell))
|
||||||
|
_config.MonitorCrossWorldLinkShell = _monitorCrossWorldLinkShell;
|
||||||
|
if (ImGui.Checkbox("Tells / Whispers", ref _monitorTell))
|
||||||
|
_config.MonitorTell = _monitorTell;
|
||||||
|
ImGui.Unindent();
|
||||||
|
|
||||||
|
ImGui.Separator();
|
||||||
|
if (ImGui.Checkbox("Debug logging (diagnose cross-world invite)", ref _debug))
|
||||||
|
_config.Debug = _debug;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"dependencies": {
|
||||||
|
"net10.0-windows7.0": {
|
||||||
|
"DalamudPackager": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[14.0.1, )",
|
||||||
|
"resolved": "14.0.1",
|
||||||
|
"contentHash": "y0WWyUE6dhpGdolK3iKgwys05/nZaVf4ZPtIjpLhJBZvHxkkiE23zYRo7K7uqAgoK/QvK5cqF6l3VG5AbgC6KA=="
|
||||||
|
},
|
||||||
|
"DotNet.ReproducibleBuilds": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[1.2.39, )",
|
||||||
|
"resolved": "1.2.39",
|
||||||
|
"contentHash": "fcFN01tDTIQqDuTwr1jUQK/geofiwjG5DycJQOnC72i1SsLAk1ELe+apBOuZ11UMQG8YKFZG1FgvjZPbqHyatg=="
|
||||||
|
},
|
||||||
|
"ffxivclientstructs": {
|
||||||
|
"type": "Project",
|
||||||
|
"dependencies": {
|
||||||
|
"InteropGenerator.Runtime": "[1.0.0, )"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"interopgenerator.runtime": {
|
||||||
|
"type": "Project"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Knack117
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# HSRTools
|
||||||
|
|
||||||
|
A [Dalamud](https://github.com/goatcorp/Dalamud) plugin for FFXIV (XIVLauncher) that automatically invites players to your party when they say a specific trigger word or phrase in chat.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Configurable trigger text** – Set any word or phrase (e.g. `inv`, `invite`, `party`). When someone types it in a monitored channel, they are invited to your party.
|
||||||
|
- **Channel selection** – Choose which channels to monitor:
|
||||||
|
- Free Company
|
||||||
|
- Link Shell (1–8)
|
||||||
|
- Cross-World Link Shell (1–8)
|
||||||
|
- Tells / Whispers
|
||||||
|
- **Case sensitivity** – Option to match the trigger with or without case sensitivity.
|
||||||
|
- **Enable/disable** – Turn the plugin on or off without uninstalling.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
1. You set a trigger word in the plugin configuration (default: `inv`).
|
||||||
|
2. The plugin listens to chat in the channels you enable (FC, LS, CWLS, tells).
|
||||||
|
3. When a message contains the trigger text, the plugin invites the **sender** of that message to your party using the game’s party invite API.
|
||||||
|
|
||||||
|
For **tells**, the game provides the sender’s content ID and world, so invites work reliably. For **FC, LS, and CWLS**, the plugin uses the sender’s content ID when available (e.g. from the last message in CWLS), so cross-world invites work for CWLS and same-world for FC/LS.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
1. Open `HSRTools.sln` in Visual Studio or use the command line.
|
||||||
|
2. Build in **Release** (or use your dev plugin folder for testing):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build HSRTools.sln -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
3. The SDK uses your XIVLauncher dev environment; the built DLL goes to your dev plugin folder.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
- Open the plugin config from the Dalamud plugin list (right-click HSRTools → **Settings**).
|
||||||
|
- Set the trigger text, enable/disable the plugin, choose channels, and set case sensitivity.
|
||||||
|
- Settings are saved when you close the game or disable the plugin.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- [XIVLauncher](https://github.com/goatcorp/FFXIVQuickLauncher) with Dalamud
|
||||||
|
- .NET (as required by your Dalamud version)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
- **From official Dalamud repo:** If this plugin is accepted, install via XIVLauncher → Dalamud Settings → Plugin Installer.
|
||||||
|
- **Third-party / dev:** Add a custom plugin repo that points to a `pluginmaster.json` (see [Releasing](#releasing)), or download the latest release zip from [Releases](https://github.com/Knack117/HSRTools/releases) and extract into your Dalamud plugin folder.
|
||||||
|
|
||||||
|
## Releasing
|
||||||
|
|
||||||
|
1. Bump `Version` in `HSRTools/HSRTools.csproj` and update `CHANGELOG.md`.
|
||||||
|
2. Build: `dotnet build HSRTools.sln -c Release`.
|
||||||
|
3. Zip the contents of `HSRTools/bin/Release/HSRTools/` (DLL + `HSRTools.json`) as `HSRTools.zip`.
|
||||||
|
4. Create a new GitHub release with tag `v1.0.0` (match version), attach `HSRTools.zip`.
|
||||||
|
5. Update `pluginmaster.json`: set `AssemblyVersion`, `LastUpdate` (Unix timestamp), and `DownloadLinkInstall` / `DownloadLinkUpdate` to the release zip URL, e.g. `https://github.com/Knack117/HSRTools/releases/download/v1.0.0/HSRTools.zip`.
|
||||||
|
6. To get on the official Dalamud repo: open a PR to [goatcorp/DalamudPlugins](https://github.com/goatcorp/DalamudPlugins) with your plugin and a `pluginmaster.json` entry (they host the zip; see their repo for the exact format).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT. See [LICENSE](LICENSE). This plugin is not officially affiliated with Square Enix or the FFXIV project.
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# Releasing HSRTools
|
||||||
|
|
||||||
|
## 1. Version and changelog
|
||||||
|
|
||||||
|
- Bump `<Version>` in `HSRTools/HSRTools.csproj` (e.g. `1.0.0.0` → `1.0.1.0`).
|
||||||
|
- Update `CHANGELOG.md` with the new version and date.
|
||||||
|
|
||||||
|
## 2. Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build HSRTools.sln -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Create release zip
|
||||||
|
|
||||||
|
Zip the **contents** of `HSRTools/bin/Release/HSRTools/` (so the zip contains `HSRTools.dll` and `HSRTools.json` at the root), and name it `HSRTools.zip`.
|
||||||
|
|
||||||
|
- **Windows (PowerShell):**
|
||||||
|
`Compress-Archive -Path "HSRTools\bin\Release\HSRTools\*" -DestinationPath "HSRTools.zip"`
|
||||||
|
- **From repo root:**
|
||||||
|
Run the above from the repo root so the archive has no extra folder layer.
|
||||||
|
|
||||||
|
## 4. GitHub release
|
||||||
|
|
||||||
|
1. Push your commits and ensure `main` is up to date.
|
||||||
|
2. Create a new release: **Releases** → **Draft a new release**.
|
||||||
|
3. **Tag:** `v1.0.0` (match the version, e.g. `v1.0.0.0` or `v1.0.0`).
|
||||||
|
4. **Title:** e.g. `v1.0.0` or `HSRTools 1.0.0`.
|
||||||
|
5. **Description:** Paste the relevant section from `CHANGELOG.md`.
|
||||||
|
6. Attach `HSRTools.zip`.
|
||||||
|
7. Publish the release.
|
||||||
|
|
||||||
|
## 5. pluginmaster.json (for third-party repo)
|
||||||
|
|
||||||
|
If you host your own plugin list:
|
||||||
|
|
||||||
|
- Set `AssemblyVersion` to the same version (e.g. `1.0.0.0`).
|
||||||
|
- Set `LastUpdate` to current Unix timestamp (e.g. `date +%s` or [epoch converter](https://www.epochconverter.com/)).
|
||||||
|
- Set `DownloadLinkInstall` and `DownloadLinkUpdate` to the zip URL, e.g.:
|
||||||
|
`https://github.com/Knack117/HSRTools/releases/download/v1.0.0/HSRTools.zip`
|
||||||
|
- Host `pluginmaster.json` at a stable URL and add that URL as a “custom plugin repository” in XIVLauncher → Dalamud Settings.
|
||||||
|
|
||||||
|
## 6. Official Dalamud repo (optional)
|
||||||
|
|
||||||
|
To get HSRTools on the main Dalamud plugin list:
|
||||||
|
|
||||||
|
1. Open [goatcorp/DalamudPlugins](https://github.com/goatcorp/DalamudPlugins) and check their README and `pluginmaster.json` format.
|
||||||
|
2. Fork, add your plugin (or a manifest entry pointing to your repo), and open a PR.
|
||||||
|
3. They may host the zip on their CDN; follow their instructions for new plugins.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"Author": "Knack117",
|
||||||
|
"Name": "HSRTools",
|
||||||
|
"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",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user