diff --git a/ipc.md b/ipc.md index 0e4983f..804699f 100755 --- a/ipc.md +++ b/ipc.md @@ -1,52 +1,104 @@ -# 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 -Hellion Chat's IPC. +This document describes the inter-plugin-communication (IPC) channels that +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 -public class ContextMenuIntegration { - // This is used to register your plugin with the IPC. It will return an ID - // that you should save for later. +public class HellionChatContextMenu { + // Used to register your plugin with the IPC; returns an ID you must save. private ICallGateSubscriber Register { get; } - // This is used to unregister your plugin from the IPC. You should call this - // when your plugin is unloaded. + // Used to unregister your plugin from the IPC; call this on unload. private ICallGateSubscriber Unregister { get; } - // You should subscribe to this event in order to receive a notification - // when Hellion Chat is loaded or updated, so you can re-register. + // Subscribe to receive a notification when Hellion Chat becomes ready + // or reloads, so you can re-register. private ICallGateSubscriber Available { get; } - // Subscribe to this to draw your custom context menu items. + // Subscribe to draw your custom context-menu items. private ICallGateSubscriber Invoke { get; } - // The registration ID. private string? _id; - public HellionChatIpc(DalamudPluginInterface @interface) { - this.Register = @interface.GetIpcSubscriber("HellionChat.Register"); + public HellionChatContextMenu(DalamudPluginInterface @interface) { + this.Register = @interface.GetIpcSubscriber("HellionChat.Register"); this.Unregister = @interface.GetIpcSubscriber("HellionChat.Unregister"); - this.Invoke = @interface.GetIpcSubscriber("HellionChat.Invoke"); - this.Available = @interface.GetIpcSubscriber("HellionChat.Available"); + this.Invoke = @interface.GetIpcSubscriber("HellionChat.Invoke"); + this.Available = @interface.GetIpcSubscriber("HellionChat.Available"); } public void Enable() { - // When Hellion Chat becomes available (if it loads after this plugin) or - // when Hellion Chat is updated, register automatically. - this.Available.Subscribe(() => this.Register()); + // Re-register automatically when Hellion Chat becomes ready or reloads. + this.Available.Subscribe(() => this.DoRegister()); // Register if Hellion Chat is already loaded. - this.Register(); + this.DoRegister(); - // Listen for context menu events. - this.Invoke.Subscribe(this.Integration); + // Listen for context-menu events. + this.Invoke.Subscribe(this.OnInvoke); } - private void Register() { - // Register and save the registration ID. + private void DoRegister() { this._id = this.Register.InvokeFunc(); } @@ -55,22 +107,21 @@ public class ContextMenuIntegration { this.Unregister.InvokeAction(this._id); this._id = null; } - - this.Invoke.Unsubscribe(this.Integration); + this.Invoke.Unsubscribe(this.OnInvoke); } - private void Integration(string id, PlayerPayload? sender, ulong contentId, Payload? payload, SeString? senderString, SeString? content) { - // Make sure the ID is the same as the saved registration ID. + private void OnInvoke(string id, PlayerPayload? sender, ulong contentId, Payload? payload, SeString? senderString, SeString? content) { + // Filter: only react to invocations that target our registration. if (id != this._id) { return; } - // Draw your custom menu items here. - // sender is the first PlayerPayload contained in the sender SeString - // contentId is the content ID of the message sender or 0 if not known - // payload is the payload that was right-clicked, if any (excluding text) - // senderString is the message sender SeString - // content is the message content SeString + // Draw your custom menu items here. Available context: + // sender — first PlayerPayload in the sender SeString, or null + // contentId — content ID of the message sender, or 0 when not known + // payload — the payload that was right-clicked, or null when text + // senderString — the full sender SeString + // content — the full message content SeString if (ImGui.Selectable("Test plugin")) { 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 -Chat's input box, subscribe to the typing IPC. -- `HellionChat.GetChatInputState`: call this function to retrieve the current state. -- `HellionChat.ChatInputStateChanged`: subscribe to this event to receive updates - whenever the state changes (and once immediately after subscribing). -Both IPC endpoints use the same tuple payload: +If your plugin already integrates with `ChatTwo.*`, the rename is the only +required change: + +```diff +-this.Register = @interface.GetIpcSubscriber("ChatTwo.Register"); +-this.Unregister = @interface.GetIpcSubscriber("ChatTwo.Unregister"); +-this.Invoke = @interface.GetIpcSubscriber<...>("ChatTwo.Invoke"); +-this.Available = @interface.GetIpcSubscriber("ChatTwo.Available"); ++this.Register = @interface.GetIpcSubscriber("HellionChat.Register"); ++this.Unregister = @interface.GetIpcSubscriber("HellionChat.Unregister"); ++this.Invoke = @interface.GetIpcSubscriber<...>("HellionChat.Invoke"); ++this.Available = @interface.GetIpcSubscriber("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) ``` -- `InputVisible`: `true` when Hellion Chat is not hidden by user/cutscene/battle - settings. -- `InputFocused`: `true` while the Hellion Chat input box currently has keyboard - focus. -- `HasText`: `true` when the input buffer contains more than whitespace. -- `IsTyping`: convenience flag (`InputFocused && HasText`). -- `TextLength`: length of the raw input buffer. -- `ChannelType`: the `HellionChat.Code.ChatType` representing the channel/mode - that will be used if the buffer is submitted. This value comes from the - current 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`. -Example usage: + +| Field | Type | Meaning | +| -------------- | ---------- | -------------------------------------------------------------------------------- | +| `InputVisible` | `bool` | True when Hellion Chat is not hidden by user, cutscene or battle settings. | +| `InputFocused` | `bool` | True while the input box currently has keyboard focus. | +| `HasText` | `bool` | True when the input buffer contains more than whitespace. | +| `IsTyping` | `bool` | Convenience flag, equivalent to `InputFocused && HasText`. | +| `TextLength` | `int` | Length of the raw input buffer in characters. | +| `ChannelType` | `ChatType` | The channel/mode that will be used if the buffer is submitted right now. | + +### Where `ChannelType` comes from + +`ChannelType` is the `HellionChat.Code.ChatType` enum value representing the +target channel for the current submission. It is sourced from the active +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 -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)> ChatInputStateChanged { get; } - public TypingIntegration(DalamudPluginInterface @interface) { - this.GetChatInputState = @interface.GetIpcSubscriber<(bool, bool, bool, bool, int, ChatType)>("HellionChat.GetChatInputState"); + + public HellionChatTypingIntegration(DalamudPluginInterface @interface) { + this.GetChatInputState = @interface.GetIpcSubscriber<(bool, bool, bool, bool, int, ChatType)>("HellionChat.GetChatInputState"); this.ChatInputStateChanged = @interface.GetIpcSubscriber<(bool, bool, bool, bool, int, ChatType)>("HellionChat.ChatInputStateChanged"); } + public void Enable() { 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(); PluginLog.Information($"Initial typing state: {state}"); } + public void Disable() { 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.