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