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:
2026-02-02 23:45:22 -05:00
commit 33088ea6e5
16 changed files with 849 additions and 0 deletions
+71
View File
@@ -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
+16
View File
@@ -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 (18), Cross-World Link Shell (18), 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.
+19
View File
@@ -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;
}
+15
View File
@@ -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>
+9
View File
@@ -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
}
+92
View File
@@ -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
}
}
}
+168
View File
@@ -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);
}
}
+136
View File
@@ -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);
}
}
}
+14
View File
@@ -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!;
}
+82
View File
@@ -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;
}
}
+28
View File
@@ -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"
}
}
}
}
+21
View File
@@ -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.
+62
View File
@@ -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 (18)
- Cross-World Link Shell (18)
- 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 games party invite API.
For **tells**, the game provides the senders content ID and world, so invites work reliably. For **FC, LS, and CWLS**, the plugin uses the senders 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.
+49
View File
@@ -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.
+19
View File
@@ -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"
}
]