docs(ipc): rewrite IPC guide in Hellion-style

Replaces the inherited Chat-2 IPC guide with a guide that follows the
Hellion README structure: top-level intro, a dedicated "Compatibility
with Chat 2" section that calls out exactly what the v1.0.0 rename did
(and did not) change, a channel-reference table, per-surface sections
with separate "Migration from Chat 2" diff blocks, and a closing
license/attribution paragraph that points back to NOTICE.md.

Content additions on top of the previous version:

- Explicit statement that the IPC surface is the Chat-2 surface re-
  published under the Hellion name. Tuple shapes, lifecycle and call
  semantics are unchanged; only the channel-string prefix differs
- Channel-reference table at the top so integrators can see the full
  surface at a glance instead of reading two long code samples
- Tuple-payload field table for the Typing State IPC with a short
  meaning per field
- Behavior notes for ChatInputStateChanged (fires once on subscribe,
  then only on real changes) so integrators don't need to read the
  source to learn the contract
- Explicit migration-diff blocks for both surfaces

Existing example code is preserved with the same identifiers; only
the host-class names are aligned to "HellionChat..." instead of
the old "ChatTwoIpc"/"TypingIntegration" placeholders.
This commit is contained in:
2026-05-03 22:32:17 +02:00
parent 76a4de1192
commit 26c12c3410
+177 -62
View File
@@ -1,33 +1,87 @@
# Context Menu IPC Integration # Hellion Chat IPC Integration Guide
If you want to display custom menu items in the chat context menu, you can use This document describes the inter-plugin-communication (IPC) channels that
Hellion Chat's IPC. Hellion Chat exposes to other Dalamud plugins. Two integration surfaces are
covered: the **Context Menu IPC** for adding custom items to Hellion Chat's
right-click menus, and the **Typing State IPC** for reacting to the user's
input-box activity.
> **Migrating from Chat 2:** the channel-name prefix changed from `ChatTwo.*` to ---
> `HellionChat.*` in v1.0.0. If you previously bound to `ChatTwo.Register` etc.,
> rename to `HellionChat.Register` etc. Tuple shapes and call semantics are
> unchanged.
Here's an example. ## Compatibility with Chat 2
Hellion Chat is a standalone fork of [Chat 2](https://github.com/Infiziert90/ChatTwo)
(EUPL-1.2). The IPC surface is one of the parts the fork inherits directly:
the same call shapes, the same tuple payloads, the same call semantics, the
same lifecycle. We did not redesign the API, we re-published it under our own
plugin name.
Concretely, this means:
- **Tuple shapes are identical.** A subscriber that worked against Chat 2's
`ChatTwo.Invoke` works against Hellion Chat's `HellionChat.Invoke` without
any code change beyond the channel string.
- **Lifecycle is identical.** The `Available` ping fires when the plugin
becomes ready, your subscriber re-registers, and the registration ID is
returned by the same `Register` call as before.
- **Channel-name prefix changed in v1.0.0.** Every `ChatTwo.*` channel name
is now `HellionChat.*`. Existing third-party integrations need a one-line
rename per channel string and nothing else.
If your plugin already supports Chat 2 and you want to add Hellion Chat
support, the cleanest path is to bind both prefixes and treat whichever one
becomes available first as the active host.
---
## Channel Reference
| Surface | Channel | Direction | Payload |
| ------------- | ------------------------------------ | --------------- | ---------------------------------------------------------------------------------------- |
| Context Menu | `HellionChat.Available` | plugin → caller | event, no payload |
| Context Menu | `HellionChat.Register` | caller → plugin | returns registration `string` ID |
| Context Menu | `HellionChat.Unregister` | caller → plugin | takes registration ID `string` |
| Context Menu | `HellionChat.Invoke` | plugin → caller | event, see Context Menu section |
| Typing State | `HellionChat.GetChatInputState` | caller → plugin | returns the typing-state tuple |
| Typing State | `HellionChat.ChatInputStateChanged` | plugin → caller | event, fires once on subscribe and on every state change, payload is the same tuple |
---
## Context Menu IPC
Use this surface to draw your own selectables inside Hellion Chat's
right-click context menus. All registrations are called inside an ImGui
`BeginMenu`, so anything you draw appears as a regular menu entry.
### Lifecycle
1. Subscribe to `HellionChat.Available`. The host fires this once when it
loads or reloads, so your plugin can re-register without polling.
2. Call `HellionChat.Register` to obtain a registration ID. Save it. You
need it to filter `Invoke` callbacks that target your registration and
to call `Unregister` later.
3. Subscribe to `HellionChat.Invoke` and draw your menu items inside the
handler when the `id` matches your saved registration ID.
4. On plugin disable or unload, call `HellionChat.Unregister` with your
saved ID and unsubscribe from `Invoke`.
### Example
```cs ```cs
public class ContextMenuIntegration { public class HellionChatContextMenu {
// This is used to register your plugin with the IPC. It will return an ID // Used to register your plugin with the IPC; returns an ID you must save.
// that you should save for later.
private ICallGateSubscriber<string> Register { get; } private ICallGateSubscriber<string> Register { get; }
// This is used to unregister your plugin from the IPC. You should call this // Used to unregister your plugin from the IPC; call this on unload.
// when your plugin is unloaded.
private ICallGateSubscriber<string, object?> Unregister { get; } private ICallGateSubscriber<string, object?> Unregister { get; }
// You should subscribe to this event in order to receive a notification // Subscribe to receive a notification when Hellion Chat becomes ready
// when Hellion Chat is loaded or updated, so you can re-register. // or reloads, so you can re-register.
private ICallGateSubscriber<object?> Available { get; } private ICallGateSubscriber<object?> Available { get; }
// Subscribe to this to draw your custom context menu items. // Subscribe to draw your custom context-menu items.
private ICallGateSubscriber<string, PlayerPayload?, ulong, Payload?, SeString?, SeString?, object?> Invoke { get; } private ICallGateSubscriber<string, PlayerPayload?, ulong, Payload?, SeString?, SeString?, object?> Invoke { get; }
// The registration ID.
private string? _id; private string? _id;
public HellionChatIpc(DalamudPluginInterface @interface) { public HellionChatContextMenu(DalamudPluginInterface @interface) {
this.Register = @interface.GetIpcSubscriber<string>("HellionChat.Register"); this.Register = @interface.GetIpcSubscriber<string>("HellionChat.Register");
this.Unregister = @interface.GetIpcSubscriber<string, object?>("HellionChat.Unregister"); this.Unregister = @interface.GetIpcSubscriber<string, object?>("HellionChat.Unregister");
this.Invoke = @interface.GetIpcSubscriber<string, PlayerPayload?, ulong, Payload?, SeString?, SeString?, object?>("HellionChat.Invoke"); this.Invoke = @interface.GetIpcSubscriber<string, PlayerPayload?, ulong, Payload?, SeString?, SeString?, object?>("HellionChat.Invoke");
@@ -35,18 +89,16 @@ public class ContextMenuIntegration {
} }
public void Enable() { public void Enable() {
// When Hellion Chat becomes available (if it loads after this plugin) or // Re-register automatically when Hellion Chat becomes ready or reloads.
// when Hellion Chat is updated, register automatically. this.Available.Subscribe(() => this.DoRegister());
this.Available.Subscribe(() => this.Register());
// Register if Hellion Chat is already loaded. // Register if Hellion Chat is already loaded.
this.Register(); this.DoRegister();
// Listen for context menu events. // Listen for context-menu events.
this.Invoke.Subscribe(this.Integration); this.Invoke.Subscribe(this.OnInvoke);
} }
private void Register() { private void DoRegister() {
// Register and save the registration ID.
this._id = this.Register.InvokeFunc(); this._id = this.Register.InvokeFunc();
} }
@@ -55,22 +107,21 @@ public class ContextMenuIntegration {
this.Unregister.InvokeAction(this._id); this.Unregister.InvokeAction(this._id);
this._id = null; this._id = null;
} }
this.Invoke.Unsubscribe(this.OnInvoke);
this.Invoke.Unsubscribe(this.Integration);
} }
private void Integration(string id, PlayerPayload? sender, ulong contentId, Payload? payload, SeString? senderString, SeString? content) { private void OnInvoke(string id, PlayerPayload? sender, ulong contentId, Payload? payload, SeString? senderString, SeString? content) {
// Make sure the ID is the same as the saved registration ID. // Filter: only react to invocations that target our registration.
if (id != this._id) { if (id != this._id) {
return; return;
} }
// Draw your custom menu items here. // Draw your custom menu items here. Available context:
// sender is the first PlayerPayload contained in the sender SeString // sender first PlayerPayload in the sender SeString, or null
// contentId is the content ID of the message sender or 0 if not known // contentId content ID of the message sender, or 0 when not known
// payload is the payload that was right-clicked, if any (excluding text) // payload the payload that was right-clicked, or null when text
// senderString is the message sender SeString // senderString the full sender SeString
// content is the message content SeString // content — the full message content SeString
if (ImGui.Selectable("Test plugin")) { if (ImGui.Selectable("Test plugin")) {
PluginLog.Log($"hi!\nsender: {sender}\ncontent id: {contentId:X}\npayload: {payload}\nsender string: {senderString}\ncontent string: {content}"); PluginLog.Log($"hi!\nsender: {sender}\ncontent id: {contentId:X}\npayload: {payload}\nsender string: {senderString}\ncontent string: {content}");
} }
@@ -78,46 +129,91 @@ public class ContextMenuIntegration {
} }
``` ```
# Typing State IPC ### Migration from Chat 2
If you need to know whether the player is currently interacting with Hellion If your plugin already integrates with `ChatTwo.*`, the rename is the only
Chat's input box, subscribe to the typing IPC. required change:
- `HellionChat.GetChatInputState`: call this function to retrieve the current state.
- `HellionChat.ChatInputStateChanged`: subscribe to this event to receive updates ```diff
whenever the state changes (and once immediately after subscribing). -this.Register = @interface.GetIpcSubscriber<string>("ChatTwo.Register");
Both IPC endpoints use the same tuple payload: -this.Unregister = @interface.GetIpcSubscriber<string, object?>("ChatTwo.Unregister");
-this.Invoke = @interface.GetIpcSubscriber<...>("ChatTwo.Invoke");
-this.Available = @interface.GetIpcSubscriber<object?>("ChatTwo.Available");
+this.Register = @interface.GetIpcSubscriber<string>("HellionChat.Register");
+this.Unregister = @interface.GetIpcSubscriber<string, object?>("HellionChat.Unregister");
+this.Invoke = @interface.GetIpcSubscriber<...>("HellionChat.Invoke");
+this.Available = @interface.GetIpcSubscriber<object?>("HellionChat.Available");
``` ```
---
## Typing State IPC
Use this surface when you need to know whether the player is currently
interacting with Hellion Chat's input box. Useful for typing indicators,
keyboard-shortcut suppression, or HUD elements that hide while the user is
typing.
### Tuple Payload
Both `HellionChat.GetChatInputState` (poll) and
`HellionChat.ChatInputStateChanged` (event) return the same tuple:
```cs
(bool InputVisible, bool InputFocused, bool HasText, bool IsTyping, int TextLength, ChatType ChannelType) (bool InputVisible, bool InputFocused, bool HasText, bool IsTyping, int TextLength, ChatType ChannelType)
``` ```
- `InputVisible`: `true` when Hellion Chat is not hidden by user/cutscene/battle
settings. | Field | Type | Meaning |
- `InputFocused`: `true` while the Hellion Chat input box currently has keyboard | -------------- | ---------- | -------------------------------------------------------------------------------- |
focus. | `InputVisible` | `bool` | True when Hellion Chat is not hidden by user, cutscene or battle settings. |
- `HasText`: `true` when the input buffer contains more than whitespace. | `InputFocused` | `bool` | True while the input box currently has keyboard focus. |
- `IsTyping`: convenience flag (`InputFocused && HasText`). | `HasText` | `bool` | True when the input buffer contains more than whitespace. |
- `TextLength`: length of the raw input buffer. | `IsTyping` | `bool` | Convenience flag, equivalent to `InputFocused && HasText`. |
- `ChannelType`: the `HellionChat.Code.ChatType` representing the channel/mode | `TextLength` | `int` | Length of the raw input buffer in characters. |
that will be used if the buffer is submitted. This value comes from the | `ChannelType` | `ChatType` | The channel/mode that will be used if the buffer is submitted right now. |
current tab's `UsedChannel` (`HellionChat/Configuration.cs`) which the plugin
keeps in sync by hooking the in-game shell (`HellionChat/GameFunctions/Chat.cs`) ### Where `ChannelType` comes from
and by resolving temporary overrides inside the chat UI
(`HellionChat/Ui/ChatLogWindow.cs:597`). `InputChannel` values are converted `ChannelType` is the `HellionChat.Code.ChatType` enum value representing the
into the exported `ChatType` via `HellionChat/Code/InputChannelExt.ToChatType`. target channel for the current submission. It is sourced from the active
Example usage: tab's `UsedChannel` (`HellionChat/Configuration.cs`), which the plugin keeps
in sync by hooking the in-game shell (`HellionChat/GameFunctions/Chat.cs`)
and by resolving temporary overrides inside the chat UI
(`HellionChat/Ui/ChatLogWindow.cs:597`). `InputChannel` values are converted
into the exported `ChatType` via
`HellionChat/Code/InputChannelExt.ToChatType`.
### Behavior
- `ChatInputStateChanged` fires once immediately after subscribe so you do
not need a separate `GetChatInputState` poll for the initial snapshot.
- After that it fires only when one or more fields actually change, so it
is safe to subscribe without rate-limiting.
- `GetChatInputState` is available for one-shot polls, e.g. on plugin
enable.
### Example
```cs ```cs
public sealed class TypingIntegration { public sealed class HellionChatTypingIntegration {
private ICallGateSubscriber<(bool InputVisible, bool InputFocused, bool HasText, bool IsTyping, int TextLength, ChatType ChannelType)> GetChatInputState { get; } private ICallGateSubscriber<(bool InputVisible, bool InputFocused, bool HasText, bool IsTyping, int TextLength, ChatType ChannelType)> GetChatInputState { get; }
private ICallGateSubscriber<(bool InputVisible, bool InputFocused, bool HasText, bool IsTyping, int TextLength, ChatType ChannelType)> ChatInputStateChanged { get; } private ICallGateSubscriber<(bool InputVisible, bool InputFocused, bool HasText, bool IsTyping, int TextLength, ChatType ChannelType)> ChatInputStateChanged { get; }
public TypingIntegration(DalamudPluginInterface @interface) {
public HellionChatTypingIntegration(DalamudPluginInterface @interface) {
this.GetChatInputState = @interface.GetIpcSubscriber<(bool, bool, bool, bool, int, ChatType)>("HellionChat.GetChatInputState"); this.GetChatInputState = @interface.GetIpcSubscriber<(bool, bool, bool, bool, int, ChatType)>("HellionChat.GetChatInputState");
this.ChatInputStateChanged = @interface.GetIpcSubscriber<(bool, bool, bool, bool, int, ChatType)>("HellionChat.ChatInputStateChanged"); this.ChatInputStateChanged = @interface.GetIpcSubscriber<(bool, bool, bool, bool, int, ChatType)>("HellionChat.ChatInputStateChanged");
} }
public void Enable() { public void Enable() {
this.ChatInputStateChanged.Subscribe(OnChatInputStateChanged); this.ChatInputStateChanged.Subscribe(OnChatInputStateChanged);
// Optionally poll the current state on enable.
// Optional: poll once for an initial snapshot. The Subscribe call
// above already fires once, so this is only useful when your code
// path needs the value before the framework hands you the event.
var state = this.GetChatInputState.InvokeFunc(); var state = this.GetChatInputState.InvokeFunc();
PluginLog.Information($"Initial typing state: {state}"); PluginLog.Information($"Initial typing state: {state}");
} }
public void Disable() { public void Disable() {
this.ChatInputStateChanged.Unsubscribe(OnChatInputStateChanged); this.ChatInputStateChanged.Unsubscribe(OnChatInputStateChanged);
} }
@@ -132,4 +228,23 @@ public sealed class TypingIntegration {
} }
``` ```
All integrations are called inside of an ImGui `BeginMenu`. ### Migration from Chat 2
Same shape as the Context Menu surface — only the channel-name prefix needs
the rename:
```diff
-this.GetChatInputState = @interface.GetIpcSubscriber<...>("ChatTwo.GetChatInputState");
-this.ChatInputStateChanged = @interface.GetIpcSubscriber<...>("ChatTwo.ChatInputStateChanged");
+this.GetChatInputState = @interface.GetIpcSubscriber<...>("HellionChat.GetChatInputState");
+this.ChatInputStateChanged = @interface.GetIpcSubscriber<...>("HellionChat.ChatInputStateChanged");
```
---
## License & Attribution
This guide and the IPC surface it documents derive directly from the Chat 2
codebase. Hellion Chat is licensed under [EUPL-1.2](LICENSE), and credit for
the original IPC design and implementation goes to **Infiziert90 (Infi)**
and **Anna Clemens** — see [`NOTICE.md`](NOTICE.md) for full attribution.