commit 33088ea6e57e8c01609994de4714cbb51d48088a Author: Knack117 Date: Mon Feb 2 23:45:22 2026 -0500 Initial release: HSRTools 1.0.0 - auto-invite on trigger word in FC/LS/CWLS/tells Co-authored-by: Cursor diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f28b7b3 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8b2498e --- /dev/null +++ b/CHANGELOG.md @@ -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. diff --git a/HSRTools.sln b/HSRTools.sln new file mode 100644 index 0000000..2a98533 --- /dev/null +++ b/HSRTools.sln @@ -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 diff --git a/HSRTools/Configuration/HSRToolsConfiguration.cs b/HSRTools/Configuration/HSRToolsConfiguration.cs new file mode 100644 index 0000000..9163db7 --- /dev/null +++ b/HSRTools/Configuration/HSRToolsConfiguration.cs @@ -0,0 +1,48 @@ +namespace HSRTools.Configuration; + +/// +/// Configuration for the HSRTools plugin. +/// +public class HSRToolsConfiguration +{ + /// + /// The trigger word or text that, when seen in chat, will invite the sender to party. + /// + public string TriggerText { get; set; } = "inv"; + + /// + /// Whether the trigger match is case-sensitive. + /// + public bool CaseSensitive { get; set; } = false; + + /// + /// Whether to monitor Free Company chat. + /// + public bool MonitorFreeCompany { get; set; } = true; + + /// + /// Whether to monitor Link Shell 1-8 chat. + /// + public bool MonitorLinkShell { get; set; } = true; + + /// + /// Whether to monitor Cross-World Link Shell 1-8 chat. + /// + public bool MonitorCrossWorldLinkShell { get; set; } = true; + + /// + /// Whether to monitor incoming tells/whispers. + /// + public bool MonitorTell { get; set; } = true; + + /// + /// Whether the plugin is enabled. + /// + public bool Enabled { get; set; } = true; + + /// + /// When true, log detailed info when a trigger fires (sender payloads, world id, invite result). + /// Use this to diagnose cross-world invite issues. + /// + public bool Debug { get; set; } = false; +} diff --git a/HSRTools/HSRTools.csproj b/HSRTools/HSRTools.csproj new file mode 100644 index 0000000..1574479 --- /dev/null +++ b/HSRTools/HSRTools.csproj @@ -0,0 +1,15 @@ + + + true + 1.0.0.0 + Knack117 + HSRTools + HSRTools + Auto-invite to party when a trigger word is said in FC, LS, CWLS, or tells. + 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). + https://github.com/Knack117/HSRTools + chat, party, invite, automation + true + + + diff --git a/HSRTools/HSRTools.json b/HSRTools/HSRTools.json new file mode 100644 index 0000000..3f3fba9 --- /dev/null +++ b/HSRTools/HSRTools.json @@ -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 +} diff --git a/HSRTools/Plugin.cs b/HSRTools/Plugin.cs new file mode 100644 index 0000000..156c347 --- /dev/null +++ b/HSRTools/Plugin.cs @@ -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(); + + _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(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 + } + } +} diff --git a/HSRTools/Services/ChatMonitorService.cs b/HSRTools/Services/ChatMonitorService.cs new file mode 100644 index 0000000..e0ebbfe --- /dev/null +++ b/HSRTools/Services/ChatMonitorService.cs @@ -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; + +/// +/// Monitors chat for the user-defined trigger and initiates party invites. +/// +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, + }; + } + + /// + /// 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. + /// + 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); + } +} diff --git a/HSRTools/Services/PartyInviteService.cs b/HSRTools/Services/PartyInviteService.cs new file mode 100644 index 0000000..7d3b2aa --- /dev/null +++ b/HSRTools/Services/PartyInviteService.cs @@ -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; + +/// +/// Service that performs party invites using the game's InfoProxyPartyInvite. +/// +public sealed class PartyInviteService +{ + private readonly IDataManager _dataManager; + private readonly IPlayerState _playerState; + + public PartyInviteService(IDataManager dataManager, IPlayerState playerState) + { + _dataManager = dataManager; + _playerState = playerState; + } + + /// + /// Resolves a world name (e.g. from "Name@Gilgamesh") to the game's world ID. + /// Returns 0 if not found. + /// + public ushort GetWorldIdByName(string worldName) + { + if (string.IsNullOrWhiteSpace(worldName)) + return 0; + + try + { + var sheet = _dataManager.GetExcelSheet(); + 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; + } + } + + /// + /// 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. + /// + 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); + } + } + + /// + /// 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. + /// + 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); + } + } + + /// + /// Gets the local player's current world ID for use when inviting same-world players. + /// + public ushort GetLocalWorldId() + { + if (!_playerState.IsLoaded) + return 0; + + var rowId = _playerState.CurrentWorld.RowId; + return rowId <= ushort.MaxValue ? (ushort)rowId : (ushort)0; + } + + /// + /// 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. + /// + public static (ulong ContentId, ushort WorldId) GetLastTellSenderInfo() + { + unsafe + { + var shell = RaptureShellModule.Instance(); + if (shell == null) + return (0, 0); + + return (shell->ContentId, shell->TellWorldId); + } + } +} diff --git a/HSRTools/Services/PluginServices.cs b/HSRTools/Services/PluginServices.cs new file mode 100644 index 0000000..231807e --- /dev/null +++ b/HSRTools/Services/PluginServices.cs @@ -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!; +} diff --git a/HSRTools/UI/ConfigWindow.cs b/HSRTools/UI/ConfigWindow.cs new file mode 100644 index 0000000..10fb357 --- /dev/null +++ b/HSRTools/UI/ConfigWindow.cs @@ -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; + } +} diff --git a/HSRTools/packages.lock.json b/HSRTools/packages.lock.json new file mode 100644 index 0000000..ed94be8 --- /dev/null +++ b/HSRTools/packages.lock.json @@ -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" + } + } + } +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ab78f63 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..cd6649a --- /dev/null +++ b/README.md @@ -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. diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..3f78f1e --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,49 @@ +# Releasing HSRTools + +## 1. Version and changelog + +- Bump `` 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. diff --git a/pluginmaster.json b/pluginmaster.json new file mode 100644 index 0000000..e7e81b3 --- /dev/null +++ b/pluginmaster.json @@ -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" + } +]