From 4674c5b73a4cb963751ba19cff8c3caff6ee8a8e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 3 May 2026 20:23:05 +0000 Subject: [PATCH 001/169] chore(actions): Bump actions/setup-dotnet from 4 to 5 Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 4 to 5. - [Release notes](https://github.com/actions/setup-dotnet/releases) - [Commits](https://github.com/actions/setup-dotnet/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-dotnet dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 2 +- .github/workflows/codeql.yml | 2 +- .github/workflows/release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c579df5..b2e4663 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: uses: actions/checkout@v6 - name: Setup .NET 10 - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: 10.0.x diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 228d646..49da25b 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -42,7 +42,7 @@ jobs: uses: actions/checkout@v6 - name: Setup .NET 10 - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: 10.0.x diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e8dd56d..6283609 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,7 +47,7 @@ jobs: ref: ${{ github.event.inputs.tag || github.ref }} - name: Setup .NET 10 - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: 10.0.x -- 2.52.0 From 76a4de1192df6753a1decb81ff38af1c9e338498 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Sun, 3 May 2026 22:29:07 +0200 Subject: [PATCH 002/169] docs(ipc): align IPC integration guide with HellionChat.* channel names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ipc.md guides third-party plugin authors who want to bind to our context-menu IPC and our typing-state IPC. After the v1.0.0 channel rename (ChatTwo.* → HellionChat.*) the example code in this file no longer matched the channels the plugin actually exposes — third-party authors who copy-pasted from the doc would see silent no-op subscriptions. Updated: - All 6 channel string literals in code samples (Register, Unregister, Invoke, Available, GetChatInputState, ChatInputStateChanged) - Code-path references in the Typing State IPC explanation block (HellionChat.Code.ChatType, HellionChat/Configuration.cs etc.) - Class/variable name in the example (ChatTwoIpc → HellionChatIpc) - Prose references to the plugin name Added a short migration note at the top so existing integrators see the rename in one paragraph instead of having to diff the file. --- ipc.md | 56 +++++++++++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/ipc.md b/ipc.md index 94aeb51..0e4983f 100755 --- a/ipc.md +++ b/ipc.md @@ -1,7 +1,12 @@ # Context Menu IPC Integration If you want to display custom menu items in the chat context menu, you can use -Chat 2's IPC. +Hellion Chat's IPC. + +> **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. @@ -14,7 +19,7 @@ public class ContextMenuIntegration { // when your plugin is unloaded. private ICallGateSubscriber Unregister { get; } // You should subscribe to this event in order to receive a notification - // when Chat 2 is loaded or updated, so you can re-register. + // when Hellion Chat is loaded or updated, so you can re-register. private ICallGateSubscriber Available { get; } // Subscribe to this to draw your custom context menu items. private ICallGateSubscriber Invoke { get; } @@ -22,18 +27,18 @@ public class ContextMenuIntegration { // The registration ID. private string? _id; - public ChatTwoIpc(DalamudPluginInterface @interface) { - this.Register = @interface.GetIpcSubscriber("ChatTwo.Register"); - this.Unregister = @interface.GetIpcSubscriber("ChatTwo.Unregister"); - this.Invoke = @interface.GetIpcSubscriber("ChatTwo.Invoke"); - this.Available = @interface.GetIpcSubscriber("ChatTwo.Available"); + public HellionChatIpc(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"); } public void Enable() { - // When Chat 2 becomes available (if it loads after this plugin) or when - // Chat 2 is updated, register automatically. + // When Hellion Chat becomes available (if it loads after this plugin) or + // when Hellion Chat is updated, register automatically. this.Available.Subscribe(() => this.Register()); - // Register if Chat 2 is already loaded. + // Register if Hellion Chat is already loaded. this.Register(); // Listen for context menu events. @@ -75,36 +80,37 @@ public class ContextMenuIntegration { # Typing State IPC -If you need to know whether the player is currently interacting with Chat 2's -input box, subscribe to the typing IPC. -- `ChatTwo.GetChatInputState`: call this function to retrieve the current state. -- `ChatTwo.ChatInputStateChanged`: subscribe to this event to receive updates +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: ``` (bool InputVisible, bool InputFocused, bool HasText, bool IsTyping, int TextLength, ChatType ChannelType) ``` -- `InputVisible`: `true` when Chat 2 is not hidden by user/cutscene/battle +- `InputVisible`: `true` when Hellion Chat is not hidden by user/cutscene/battle settings. -- `InputFocused`: `true` while the Chat 2 input box currently has keyboard focus. +- `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 `ChatTwo.Code.ChatType` representing the channel/mode that - will be used if the buffer is submitted. This value comes from the current - tab's `UsedChannel` (`ChatTwo/Configuration.cs`) which the plugin keeps in - sync by hooking the in-game shell (`ChatTwo/GameFunctions/Chat.cs`) and by - resolving temporary overrides inside the chat UI - (`ChatTwo/Ui/ChatLogWindow.cs:597`). `InputChannel` values are converted into - the exported `ChatType` via `ChatTwo/Code/InputChannelExt.ToChatType`. +- `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: ```cs public sealed class TypingIntegration { 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)>("ChatTwo.GetChatInputState"); - this.ChatInputStateChanged = @interface.GetIpcSubscriber<(bool, bool, bool, bool, int, ChatType)>("ChatTwo.ChatInputStateChanged"); + 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); -- 2.52.0 From 26c12c3410a0ab0cdfe5504e49289928e4f3b359 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Sun, 3 May 2026 22:32:17 +0200 Subject: [PATCH 003/169] 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. --- ipc.md | 247 ++++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 181 insertions(+), 66 deletions(-) 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. -- 2.52.0 From 4c18b9a62bd6e9ae1fb13d16aba2ed1ceeaac098 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Sun, 3 May 2026 22:48:21 +0200 Subject: [PATCH 004/169] feat(tabs): sharpen default tab layout, hard-reset on v12 -> v13 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aligns the first-run tab layout with the sharpened defaults that external testers asked for. Three changes in one commit: 1. VanillaGeneral now contains only Say/Yell/Shout. The previous 30-channel kitchen-sink (party, FC, every linkshell, all gameplay events) buried the actual immediate-surroundings conversation under loot rolls, crafting and PF pings. 2. HellionSystem absorbs the gameplay-event streams that used to live in General — NpcDialogue, LootNotice, LootRoll, Crafting, Gathering, PeriodicRecruitmentNotification — plus the announcement and battle- system noise (BattleSystem, FreeCompanyAnnouncement, PvpTeamAnnouncement) that previously had no fixed home. 3. The first-run / wipe default no longer adds HellionBeginner conditionally and no longer adds a static VanillaTellExclusive tab. Auto-Tell-Tabs spawns per-conversation tabs on demand, the static tell-bucket is redundant. NoviceNetwork users can still add the Beginner preset from Settings -> Tabs. A new v12 -> v13 migration triggers a hard tab-wipe on existing installs because per-channel mapping from the old General preset to the new General/System split is ambiguous. The wipe scope is narrow: only Config.Tabs is cleared, every other knob (Privacy, Retention, Theme, etc.) keeps its current value. A pre-v13 backup of the live config is written alongside it for manual restore. Users see the existing SettingsRefactor migration notification. --- HellionChat/Plugin.cs | 64 +++++++++++++++++++++++++++++++----- HellionChat/Util/TabsUtil.cs | 53 +++++++++++------------------ 2 files changed, 74 insertions(+), 43 deletions(-) diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index 4aba746..4bc24d8 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -190,22 +190,68 @@ public sealed class Plugin : IDalamudPlugin "SeenPopOutHeaderHint reset to false (v0.6.1 banner re-armed)"); } - // Hellion default tab layout for first-run and v10-wipe. - // General catches player chat plus active gameplay events; the - // System tab takes the technical noise so it does not bury real - // conversation. Beginner tab only appears when the Novice - // Network is enabled in Audio and Notifications, otherwise it - // would just sit empty. + // Hellion Chat v12 → v13 — hard-resets the tab layout to the + // sharpened v1.0.0 defaults (5 thematic tabs, see TabsUtil and + // the default-fill block below). Existing tab state is wiped + // because per-channel mapping from the old General preset to + // the new General/System split would be ambiguous and would + // produce subtly wrong results for users who tweaked the old + // layout. A timestamped backup of the live config is written + // alongside it as a manual restore safety net. The wipe scope + // is intentionally narrow: only Config.Tabs is reset; Privacy, + // Retention, Theme and every other knob keeps its current value. + if (Config.Version < 13) + { + var pluginConfigsDir = Interface.ConfigDirectory.Parent?.FullName; + if (pluginConfigsDir is not null) + { + var liveConfigPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json"); + var backupPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json.pre-v13-backup"); + + try + { + if (File.Exists(liveConfigPath)) + File.Copy(liveConfigPath, backupPath, overwrite: true); + } + catch (Exception ex) + { + Log.Warning(ex, "HellionChat: pre-v13 config backup failed"); + } + } + + Config.Tabs.Clear(); + Config.Version = 13; + SaveConfig(); + + Log.Information( + "Migrated config v12 → v13: tab layout hard-reset to v1.0.0 defaults; " + + "pre-v13 config backup written next to the live file. " + + "Default tabs will be populated by the Tabs.Count == 0 block."); + + Notification.AddNotification(new Dalamud.Interface.ImGuiNotification.Notification + { + Title = HellionStrings.SettingsRefactor_Migration_Title, + Content = HellionStrings.SettingsRefactor_Migration_Content, + Type = Dalamud.Interface.ImGuiNotification.NotificationType.Info, + InitialDuration = TimeSpan.FromSeconds(25), + }); + } + + // Hellion v1.0.0 default tab layout. Five thematically separated + // tabs: General catches the immediate-surroundings public chat + // (Say/Yell/Shout) only; System absorbs the rest of the technical + // and gameplay-event noise; FreeCompany, Group and Linkshell each + // own their respective channel set. Tells are not in a static + // tab anymore — Auto-Tell-Tabs spawns dedicated per-conversation + // tabs on demand. Novice-Network gets no preset tab; users who + // want it can add HellionBeginner from Settings → Tabs. if (Config.Tabs.Count == 0) { Config.Tabs.Add(TabsUtil.VanillaGeneral); Config.Tabs.Add(TabsUtil.HellionSystem); Config.Tabs.Add(TabsUtil.HellionFreeCompany); Config.Tabs.Add(TabsUtil.HellionParty); - if (Config.ShowNoviceNetwork) - Config.Tabs.Add(TabsUtil.HellionBeginner); Config.Tabs.Add(TabsUtil.HellionLinkshell); - Config.Tabs.Add(TabsUtil.VanillaTellExclusive); } LanguageChanged(Interface.UiLanguage); diff --git a/HellionChat/Util/TabsUtil.cs b/HellionChat/Util/TabsUtil.cs index 4c0a762..bd7e39d 100755 --- a/HellionChat/Util/TabsUtil.cs +++ b/HellionChat/Util/TabsUtil.cs @@ -14,48 +14,20 @@ public static class TabsUtil return channels; } - // Hellion-tuned General preset. The pure player-talk catch-all plus - // the active-gameplay event streams (loot, crafting, gathering, NPC - // dialogue, party-finder pings). Pure technical noise (System, Error, - // Login/Logout spam, retainer sales, alarms, sign messages) lives in - // the dedicated System tab so it doesn't bury actual conversation. + // Hellion-tuned General preset (v1.0.0 — sharpened defaults). + // Public-chat-only, the bare three channels you encounter in open + // world. Group/FC/Linkshell traffic moves to dedicated tabs, gameplay + // events (loot, crafting, gathering, NPC dialogue, PF pings) move to + // the System tab where they belong — keeps the General view focused + // on actual conversation in the immediate surroundings. public static Tab VanillaGeneral => new() { Name = Language.Tabs_Presets_General, SelectedChannels = new Dictionary { - // Player chat [ChatType.Say] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.Yell] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.Shout] = (ChatSourceExt.All, ChatSourceExt.All), - [ChatType.Party] = (ChatSourceExt.All, ChatSourceExt.All), - [ChatType.CrossParty] = (ChatSourceExt.All, ChatSourceExt.All), - [ChatType.Alliance] = (ChatSourceExt.All, ChatSourceExt.All), - [ChatType.FreeCompany] = (ChatSourceExt.All, ChatSourceExt.All), - [ChatType.PvpTeam] = (ChatSourceExt.All, ChatSourceExt.All), - [ChatType.CrossLinkshell1] = (ChatSourceExt.All, ChatSourceExt.All), - [ChatType.CrossLinkshell2] = (ChatSourceExt.All, ChatSourceExt.All), - [ChatType.CrossLinkshell3] = (ChatSourceExt.All, ChatSourceExt.All), - [ChatType.CrossLinkshell4] = (ChatSourceExt.All, ChatSourceExt.All), - [ChatType.CrossLinkshell5] = (ChatSourceExt.All, ChatSourceExt.All), - [ChatType.CrossLinkshell6] = (ChatSourceExt.All, ChatSourceExt.All), - [ChatType.CrossLinkshell7] = (ChatSourceExt.All, ChatSourceExt.All), - [ChatType.CrossLinkshell8] = (ChatSourceExt.All, ChatSourceExt.All), - [ChatType.Linkshell1] = (ChatSourceExt.All, ChatSourceExt.All), - [ChatType.Linkshell2] = (ChatSourceExt.All, ChatSourceExt.All), - [ChatType.Linkshell3] = (ChatSourceExt.All, ChatSourceExt.All), - [ChatType.Linkshell4] = (ChatSourceExt.All, ChatSourceExt.All), - [ChatType.Linkshell5] = (ChatSourceExt.All, ChatSourceExt.All), - [ChatType.Linkshell6] = (ChatSourceExt.All, ChatSourceExt.All), - [ChatType.Linkshell7] = (ChatSourceExt.All, ChatSourceExt.All), - [ChatType.Linkshell8] = (ChatSourceExt.All, ChatSourceExt.All), - // Active-gameplay events - [ChatType.NpcDialogue] = (ChatSourceExt.All, ChatSourceExt.All), - [ChatType.LootNotice] = (ChatSourceExt.All, ChatSourceExt.All), - [ChatType.LootRoll] = (ChatSourceExt.All, ChatSourceExt.All), - [ChatType.Crafting] = (ChatSourceExt.All, ChatSourceExt.All), - [ChatType.Gathering] = (ChatSource.LocalPlayer, ChatSource.LocalPlayer), - [ChatType.PeriodicRecruitmentNotification] = (ChatSourceExt.All, ChatSourceExt.All), } }; @@ -130,6 +102,7 @@ public static class TabsUtil Name = HellionStrings.Tabs_Presets_System, SelectedChannels = new Dictionary { + // Plain system noise [ChatType.Debug] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.Urgent] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.Notice] = (ChatSourceExt.All, ChatSourceExt.All), @@ -138,10 +111,22 @@ public static class TabsUtil [ChatType.Echo] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.GatheringSystem] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.NoviceNetworkSystem] = (ChatSourceExt.All, ChatSourceExt.All), + [ChatType.BattleSystem] = (ChatSourceExt.All, ChatSourceExt.All), + // Login / logout / announcement noise [ChatType.NpcAnnouncement] = (ChatSourceExt.All, ChatSourceExt.All), + [ChatType.FreeCompanyAnnouncement] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.FreeCompanyLoginLogout] = (ChatSourceExt.All, ChatSourceExt.All), + [ChatType.PvpTeamAnnouncement] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.PvpTeamLoginLogout] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.RetainerSale] = (ChatSourceExt.All, ChatSourceExt.All), + [ChatType.PeriodicRecruitmentNotification] = (ChatSourceExt.All, ChatSourceExt.All), + // Gameplay-event streams (moved out of General in v1.0.0) + [ChatType.NpcDialogue] = (ChatSourceExt.All, ChatSourceExt.All), + [ChatType.LootNotice] = (ChatSourceExt.All, ChatSourceExt.All), + [ChatType.LootRoll] = (ChatSourceExt.All, ChatSourceExt.All), + [ChatType.Crafting] = (ChatSourceExt.All, ChatSourceExt.All), + [ChatType.Gathering] = (ChatSourceExt.All, ChatSourceExt.All), + // Misc [ChatType.Progress] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.RandomNumber] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.Orchestrion] = (ChatSourceExt.All, ChatSourceExt.All), -- 2.52.0 From fa9baa392908bc41d4849e791a2f0669e435e199 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Sun, 3 May 2026 22:51:06 +0200 Subject: [PATCH 005/169] docs(changelog): document v12 -> v13 tab reset and pre-v13 backup Adds a "Default tab layout sharpened" block between the Safety and Crash-class sections in both yaml and repo.json. Explains the new five-tab structure, calls out that the reset is one-time, that all non-tab settings are preserved, and that the live config is backed up to pluginConfigs/HellionChat.json.pre-v13-backup before the wipe so users can restore manually. The actual code change shipped in the previous commit; this commit is purely the user-facing communication so the in-game migration notification has matching written context. --- HellionChat/HellionChat.yaml | 20 ++++++++++++++++++++ repo.json | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/HellionChat/HellionChat.yaml b/HellionChat/HellionChat.yaml index d264e52..e5f76d3 100755 --- a/HellionChat/HellionChat.yaml +++ b/HellionChat/HellionChat.yaml @@ -83,6 +83,26 @@ changelog: |- - NuGet restore now honors packages.lock.json so transitive dependencies don't drift between machines or CI runs + Default tab layout sharpened (one-time tab reset on first start): + + The first-run tab layout is reorganized into five thematic tabs + based on external tester feedback. General contains only Say, + Yell and Shout (immediate-surroundings public chat). System + absorbs the gameplay-event streams (NpcDialogue, Loot, Crafting, + Gathering, PF recruitment pings) and announcement noise + (BattleSystem, FreeCompanyAnnouncement, PvpTeamAnnouncement) + that previously lived in General. FreeCompany, Group and + Linkshell each own their channel set. The static Tell tab is + gone — Auto-Tell-Tabs spawns per-conversation tabs on demand. + The Beginner / Novice-Network preset is no longer added by + default but is still available via Settings, Tabs. + + This is a one-time tab-layout reset for users on config version + 12 or older. Privacy, Retention, Theme and every other setting + is preserved. Your previous tab configuration is written to + pluginConfigs/HellionChat.json.pre-v13-backup so you can restore + it manually if you prefer the old layout. + Crash-class fixes (formerly latent in upstream): - MathUtil.HasOverlap now uses a correct AABB test; identical or diff --git a/repo.json b/repo.json index f412f7f..d7e8c28 100644 --- a/repo.json +++ b/repo.json @@ -20,7 +20,7 @@ "CanUnloadAsync": false, "LoadPriority": 0, "Punchline": "Chat replacement with privacy controls aligned to EU, US and JP rules — based on Chat 2 (EUPL-1.2)", - "Changelog": "**Hellion Chat 1.0.0 — Standalone Major Release**\n\nFirst fully standalone release. Internal cleanup plus a sweep of\npre-existing correctness, security, threading and resource-leak\nfixes carried over from the upstream codebase. No user action\nrequired — auto-update applies cleanly, configuration and database\npaths unchanged.\n\nStandalone identity:\n\n- Code namespace consolidated from ChatTwo.* to HellionChat.* across\n all source files\n- IPC channels migrated from ChatTwo.* to HellionChat.* (6 channels:\n Register, Available, Unregister, Invoke, GetChatInputState,\n ChatInputStateChanged) — third-party plugins that bound to the old\n channels need to be updated; none known at release time\n- ImGui popup ID renamed to hellionchat-context-popup\n- Repository folder restructured (ChatTwo/ → HellionChat/), all CI\n and build paths updated accordingly\n- Public-facing descriptions reworded from upstream-fork framing to\n standalone framing (Chat 2 attribution preserved per EUPL-1.2)\n- Colour preset 'ChatTwo Default' is now 'Klassik (Chat 2 Default)'\n\nSafety:\n\n- Plugin now refuses to load when upstream Chat 2 is also active —\n bilingual conflict message in EN/DE, throw before any subsystem\n initialization, prevents the runtime crash that previously occurred\n when both plugins replaced the same chat window in parallel\n- SQLite native binary bumped to 3.50.3 (CVE-2025-6965 memory\n corruption from aggregate-term overflow, CVE-2025-7709)\n- NuGet restore now honors packages.lock.json so transitive\n dependencies don't drift between machines or CI runs\n\nCrash-class fixes (formerly latent in upstream):\n\n- MathUtil.HasOverlap now uses a correct AABB test; identical or\n edge-touching rectangles are no longer reported as non-overlapping\n- ChatCode.Equals compares fields directly instead of GetHashCode;\n removes the hash-collision anti-pattern\n- IpcManager.Dispose uses UnregisterAction to match the matching\n RegisterAction call; previous mismatch leaked the action\n subscription on every plugin reload\n- ExtraChat.Dispose now unsubscribes all three IPC subscriptions\n (was only the first); leaks closed\n- TellTarget.FromTarget guards against a zero IPlayerCharacter.Address\n before dereferencing the unsafe Character* cast\n- GameFunctions ResolveTextCommandPlaceholderDetour null-checks the\n Hook reference instead of using the null-forgiving operator\n- Popout.cs and SettingsTabs/Tabs.cs bounds-check list indexing so\n a tab drop or empty-worlds list no longer crashes the UI\n- Debugger.cs now declares IDisposable so the existing Dispose runs\n\nCorrectness fixes:\n\n- GlobalParametersCache.GetValue captures Cache into a local before\n the bounds check, so a concurrent Refresh can't slip a different\n array between check and read\n- IconUtil binary search bounds initialized to entries.Length-1 and\n reset on redirect-restart; entries.Length==0 short-circuits\n- Sheets.WorldsOnDatacenter now compares DataCenter.RowId (was\n Region.RowId) so it actually returns same-DC worlds\n- Message.cs back-reference loop iterates the processed Sender/Content\n properties so chunks added by CheckMessageContent get Message set\n- Language.zh-Hans Webinterface_Start_Success corrected to\n \"网页界面已启动\" (was \"网页界面已停止\")\n\nThreading and async:\n\n- AutoTranslate Entries/ValidEntries are now serialized behind a\n single lock; the preload worker thread and main thread no longer\n race on the underlying dictionary/hash set\n- Privacy retention and cleanup workers bound their framework-refresh\n waits to 5 seconds with a logged timeout; a hung framework tick can\n no longer deadlock the background worker\n\nResource handling:\n\n- EmoteCache reuses the static HttpClient instead of allocating a new\n one per call (closed socket leak)\n- FontManager wraps HttpClient/HttpResponseMessage in using-blocks\n and adds EnsureSuccessStatusCode; failed downloads no longer\n silently produce a zero-byte font file\n- SearchSelector mixes the row index into the ImGui ID stack so\n selectables don't collapse to a single ambiguous ID\n- SettingsTabs/Chat blocked-emote add-button now opens its selector\n popup on left-click\n\nPerformance:\n\n- DbViewer text export caches filteredHistory.Count once instead of\n re-enumerating the IEnumerable on every batch (O(N) instead of\n O(N²) on large histories)\n\nLicense attribution (NOTICE.md, COPYRIGHT, THIRD_PARTY_NOTICES.md\nand the Credits section in README) is unchanged.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 0.6.1 — Pop-Out Discoverability & /tell Auto-Pop-Out**\n\n- Pop-out button now visible in the chat header (no more hunting through the right-click menu)\n- One-time hint banner explains pop-out tabs and the right-click shortcut\n- New setting: open new /tell tabs directly as pop-out windows (Settings → Chat → Auto-Tell-Tabs)\n- Pop-out input is now enabled by default — closing a pop-out still returns the tab to the sidebar\n- Bugfix: dropping or logging out with an LRU/popped auto-tell tab now also closes its pop-out window (no more ghost windows)\n- Bugfix: dead zone below the chat input bar when the v0.6.0 pop-out hint banner was visible (also fixed retroactively for the v0.6.0 banner inside pop-outs)\n\nModding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 0.6.0 — UX Polish: Pop-Out Input + Colour Presets**\n\nTwo opt-in UX features land in the same release. Existing users see\nno change unless they enable the new toggles.\n\nPop-out input bar:\n\n- New global master switch in Settings → Window → Frame: \"Enable input\n in pop-outs\". Default OFF so existing behaviour is preserved\n- When enabled, every pop-out window grows a compact input bar at the\n bottom (channel-coloured icon button left, text input right). The\n auto-translate picker is intentionally not part of the compact bar\n in v0.6.0 — typical pop-out workflows (FC greeter, club hostess)\n rarely need it there\n- Each pop-out keeps an independent text buffer and history cursor;\n channel changes still apply globally because that is how the FFXIV\n channel API works\n- Up/Down navigates a shared input history singleton across the main\n window and every open pop-out\n- First pop-out opening after the upgrade shows a one-time hint\n banner pointing users to the new toggle\n\nChat colour presets:\n\n- Seven built-in presets above the per-channel colour list in\n Settings → Appearance → Colours: ChatTwo Default, High-Contrast,\n Pastell, Dark-Mode-Tuned, Hellion (brand-coloured, blue/orange\n Arctic Cyan + Ember Glow palette from the Hellion Online Media\n branding spec), plus two bonus mood presets — Night Blue (royal\n blue, classic-cool) and Indigo Violet (royal violet, glitter-mystic)\n- Apply is immediate and overwrites the channels covered by the\n preset; battle-channel colours are left alone so combat tuning\n stays intact\n\nConfiguration migrates from v10 to v11 with a diagnostic log entry;\nno data is reset. Bilingual (English/German) for both new sections.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 0.5.4 — WrapText hardening**\n\nReplaces the unsafe pointer-arithmetic in ImGuiUtil.WrapText with\nSpan- and index-based control flow. Closes the persistent CodeQL\nCritical alert \"unvalidated local pointer arithmetic\" that kept\nre-firing on every shape of the previous fix.\n\nHardening:\n\n- WrapText now allocates a buffer sized by Encoding.UTF8.GetMaxByteCount\n via ArrayPool, validates the actual encoded length against that\n ceiling, and threads the rest of the algorithm through int offsets\n instead of raw byte pointers\n- Pointer arithmetic only happens inside two small private helpers\n (CalcWordWrap and DrawText) that take the pinned base pointer plus\n int offsets sourced from the plugin's own logic, not from any\n virtual-method return\n- Added a 16 KiB upper bound on the buffer rent to prevent a\n pathological input from triggering an unbounded ArrayPool allocation\n\nNo user-visible behaviour change. Word-wrap output is byte-identical\nto v0.5.3.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 0.5.3 — Pointer arithmetic hardening**\n\nClosed CodeQL Critical alert in ImGuiUtil.WrapText by validating the\nencoded byte buffer length via GetByteCount before pointer\narithmetic. Single-fix patch on top of v0.5.2.\n\n---\n\nEarlier history: https://github.com/JonKazama-Hellion/HellionChat/releases", + "Changelog": "**Hellion Chat 1.0.0 — Standalone Major Release**\n\nFirst fully standalone release. Internal cleanup plus a sweep of\npre-existing correctness, security, threading and resource-leak\nfixes carried over from the upstream codebase. No user action\nrequired — auto-update applies cleanly, configuration and database\npaths unchanged.\n\nStandalone identity:\n\n- Code namespace consolidated from ChatTwo.* to HellionChat.* across\n all source files\n- IPC channels migrated from ChatTwo.* to HellionChat.* (6 channels:\n Register, Available, Unregister, Invoke, GetChatInputState,\n ChatInputStateChanged) — third-party plugins that bound to the old\n channels need to be updated; none known at release time\n- ImGui popup ID renamed to hellionchat-context-popup\n- Repository folder restructured (ChatTwo/ → HellionChat/), all CI\n and build paths updated accordingly\n- Public-facing descriptions reworded from upstream-fork framing to\n standalone framing (Chat 2 attribution preserved per EUPL-1.2)\n- Colour preset 'ChatTwo Default' is now 'Klassik (Chat 2 Default)'\n\nSafety:\n\n- Plugin now refuses to load when upstream Chat 2 is also active —\n bilingual conflict message in EN/DE, throw before any subsystem\n initialization, prevents the runtime crash that previously occurred\n when both plugins replaced the same chat window in parallel\n- SQLite native binary bumped to 3.50.3 (CVE-2025-6965 memory\n corruption from aggregate-term overflow, CVE-2025-7709)\n- NuGet restore now honors packages.lock.json so transitive\n dependencies don't drift between machines or CI runs\n\nDefault tab layout sharpened (one-time tab reset on first start):\n\nThe first-run tab layout is reorganized into five thematic tabs\nbased on external tester feedback. General contains only Say,\nYell and Shout (immediate-surroundings public chat). System\nabsorbs the gameplay-event streams (NpcDialogue, Loot, Crafting,\nGathering, PF recruitment pings) and announcement noise\n(BattleSystem, FreeCompanyAnnouncement, PvpTeamAnnouncement)\nthat previously lived in General. FreeCompany, Group and\nLinkshell each own their channel set. The static Tell tab is\ngone — Auto-Tell-Tabs spawns per-conversation tabs on demand.\nThe Beginner / Novice-Network preset is no longer added by\ndefault but is still available via Settings, Tabs.\n\nThis is a one-time tab-layout reset for users on config version\n12 or older. Privacy, Retention, Theme and every other setting\nis preserved. Your previous tab configuration is written to\npluginConfigs/HellionChat.json.pre-v13-backup so you can restore\nit manually if you prefer the old layout.\n\nCrash-class fixes (formerly latent in upstream):\n\n- MathUtil.HasOverlap now uses a correct AABB test; identical or\n edge-touching rectangles are no longer reported as non-overlapping\n- ChatCode.Equals compares fields directly instead of GetHashCode;\n removes the hash-collision anti-pattern\n- IpcManager.Dispose uses UnregisterAction to match the matching\n RegisterAction call; previous mismatch leaked the action\n subscription on every plugin reload\n- ExtraChat.Dispose now unsubscribes all three IPC subscriptions\n (was only the first); leaks closed\n- TellTarget.FromTarget guards against a zero IPlayerCharacter.Address\n before dereferencing the unsafe Character* cast\n- GameFunctions ResolveTextCommandPlaceholderDetour null-checks the\n Hook reference instead of using the null-forgiving operator\n- Popout.cs and SettingsTabs/Tabs.cs bounds-check list indexing so\n a tab drop or empty-worlds list no longer crashes the UI\n- Debugger.cs now declares IDisposable so the existing Dispose runs\n\nCorrectness fixes:\n\n- GlobalParametersCache.GetValue captures Cache into a local before\n the bounds check, so a concurrent Refresh can't slip a different\n array between check and read\n- IconUtil binary search bounds initialized to entries.Length-1 and\n reset on redirect-restart; entries.Length==0 short-circuits\n- Sheets.WorldsOnDatacenter now compares DataCenter.RowId (was\n Region.RowId) so it actually returns same-DC worlds\n- Message.cs back-reference loop iterates the processed Sender/Content\n properties so chunks added by CheckMessageContent get Message set\n- Language.zh-Hans Webinterface_Start_Success corrected to\n \"网页界面已启动\" (was \"网页界面已停止\")\n\nThreading and async:\n\n- AutoTranslate Entries/ValidEntries are now serialized behind a\n single lock; the preload worker thread and main thread no longer\n race on the underlying dictionary/hash set\n- Privacy retention and cleanup workers bound their framework-refresh\n waits to 5 seconds with a logged timeout; a hung framework tick can\n no longer deadlock the background worker\n\nResource handling:\n\n- EmoteCache reuses the static HttpClient instead of allocating a new\n one per call (closed socket leak)\n- FontManager wraps HttpClient/HttpResponseMessage in using-blocks\n and adds EnsureSuccessStatusCode; failed downloads no longer\n silently produce a zero-byte font file\n- SearchSelector mixes the row index into the ImGui ID stack so\n selectables don't collapse to a single ambiguous ID\n- SettingsTabs/Chat blocked-emote add-button now opens its selector\n popup on left-click\n\nPerformance:\n\n- DbViewer text export caches filteredHistory.Count once instead of\n re-enumerating the IEnumerable on every batch (O(N) instead of\n O(N²) on large histories)\n\nLicense attribution (NOTICE.md, COPYRIGHT, THIRD_PARTY_NOTICES.md\nand the Credits section in README) is unchanged.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 0.6.1 — Pop-Out Discoverability & /tell Auto-Pop-Out**\n\n- Pop-out button now visible in the chat header (no more hunting through the right-click menu)\n- One-time hint banner explains pop-out tabs and the right-click shortcut\n- New setting: open new /tell tabs directly as pop-out windows (Settings → Chat → Auto-Tell-Tabs)\n- Pop-out input is now enabled by default — closing a pop-out still returns the tab to the sidebar\n- Bugfix: dropping or logging out with an LRU/popped auto-tell tab now also closes its pop-out window (no more ghost windows)\n- Bugfix: dead zone below the chat input bar when the v0.6.0 pop-out hint banner was visible (also fixed retroactively for the v0.6.0 banner inside pop-outs)\n\nModding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 0.6.0 — UX Polish: Pop-Out Input + Colour Presets**\n\nTwo opt-in UX features land in the same release. Existing users see\nno change unless they enable the new toggles.\n\nPop-out input bar:\n\n- New global master switch in Settings → Window → Frame: \"Enable input\n in pop-outs\". Default OFF so existing behaviour is preserved\n- When enabled, every pop-out window grows a compact input bar at the\n bottom (channel-coloured icon button left, text input right). The\n auto-translate picker is intentionally not part of the compact bar\n in v0.6.0 — typical pop-out workflows (FC greeter, club hostess)\n rarely need it there\n- Each pop-out keeps an independent text buffer and history cursor;\n channel changes still apply globally because that is how the FFXIV\n channel API works\n- Up/Down navigates a shared input history singleton across the main\n window and every open pop-out\n- First pop-out opening after the upgrade shows a one-time hint\n banner pointing users to the new toggle\n\nChat colour presets:\n\n- Seven built-in presets above the per-channel colour list in\n Settings → Appearance → Colours: ChatTwo Default, High-Contrast,\n Pastell, Dark-Mode-Tuned, Hellion (brand-coloured, blue/orange\n Arctic Cyan + Ember Glow palette from the Hellion Online Media\n branding spec), plus two bonus mood presets — Night Blue (royal\n blue, classic-cool) and Indigo Violet (royal violet, glitter-mystic)\n- Apply is immediate and overwrites the channels covered by the\n preset; battle-channel colours are left alone so combat tuning\n stays intact\n\nConfiguration migrates from v10 to v11 with a diagnostic log entry;\nno data is reset. Bilingual (English/German) for both new sections.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 0.5.4 — WrapText hardening**\n\nReplaces the unsafe pointer-arithmetic in ImGuiUtil.WrapText with\nSpan- and index-based control flow. Closes the persistent CodeQL\nCritical alert \"unvalidated local pointer arithmetic\" that kept\nre-firing on every shape of the previous fix.\n\nHardening:\n\n- WrapText now allocates a buffer sized by Encoding.UTF8.GetMaxByteCount\n via ArrayPool, validates the actual encoded length against that\n ceiling, and threads the rest of the algorithm through int offsets\n instead of raw byte pointers\n- Pointer arithmetic only happens inside two small private helpers\n (CalcWordWrap and DrawText) that take the pinned base pointer plus\n int offsets sourced from the plugin's own logic, not from any\n virtual-method return\n- Added a 16 KiB upper bound on the buffer rent to prevent a\n pathological input from triggering an unbounded ArrayPool allocation\n\nNo user-visible behaviour change. Word-wrap output is byte-identical\nto v0.5.3.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 0.5.3 — Pointer arithmetic hardening**\n\nClosed CodeQL Critical alert in ImGuiUtil.WrapText by validating the\nencoded byte buffer length via GetByteCount before pointer\narithmetic. Single-fix patch on top of v0.5.2.\n\n---\n\nEarlier history: https://github.com/JonKazama-Hellion/HellionChat/releases", "AcceptsFeedback": true, "DownloadLinkInstall": "https://github.com/JonKazama-Hellion/HellionChat/releases/download/v1.0.0/latest.zip", "DownloadLinkUpdate": "https://github.com/JonKazama-Hellion/HellionChat/releases/download/v1.0.0/latest.zip", -- 2.52.0 From d63c7108368c5783283d4fb3172f66681a7c9c89 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Mon, 4 May 2026 09:03:59 +0200 Subject: [PATCH 006/169] docs: restructure into docs/ folder, add roadmap and learning notes - Move AI_DISCLOSURE, THIRD_PARTY_NOTICES, UPSTREAM_SYNC, ipc.md into docs/ (ipc.md renamed to IPC.md for consistency) - Add docs/ROADMAP.md, docs/CHANGELOG.md, docs/CONTRIBUTORS.md, docs/LEARNING-JOURNEY.md - Update README to reflect the v1.0.0 standalone state, drop the development section, refresh the architecture tree, add a release-cadence block linking to LEARNING-JOURNEY - Fix stale ChatTwo/* source paths to HellionChat/* across docs - Update cross-links in PRIVACY, CONTRIBUTING and .github/* so they point at the new docs/ paths Pure documentation pass, no code changes. --- .github/PULL_REQUEST_TEMPLATE.md | 4 +- .github/release-footer.md | 2 +- CONTRIBUTING.md | 21 +- PRIVACY.md | 8 +- README.md | 154 ++++++------ AI_DISCLOSURE.md => docs/AI_DISCLOSURE.md | 2 +- docs/CHANGELOG.md | 83 +++++++ docs/CONTRIBUTORS.md | 61 +++++ ipc.md => docs/IPC.md | 0 docs/LEARNING-JOURNEY.md | 219 ++++++++++++++++++ docs/ROADMAP.md | 131 +++++++++++ .../THIRD_PARTY_NOTICES.md | 12 +- UPSTREAM_SYNC.md => docs/UPSTREAM_SYNC.md | 0 13 files changed, 584 insertions(+), 113 deletions(-) rename AI_DISCLOSURE.md => docs/AI_DISCLOSURE.md (97%) create mode 100644 docs/CHANGELOG.md create mode 100644 docs/CONTRIBUTORS.md rename ipc.md => docs/IPC.md (100%) create mode 100644 docs/LEARNING-JOURNEY.md create mode 100644 docs/ROADMAP.md rename THIRD_PARTY_NOTICES.md => docs/THIRD_PARTY_NOTICES.md (90%) rename UPSTREAM_SYNC.md => docs/UPSTREAM_SYNC.md (100%) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index f5cf909..d4542c0 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -53,7 +53,7 @@ new commands, new translations, removed behaviour. If none, write bump and is it covered by the existing migration tests? - Does this change the schema in MessageStore? - Does this change the repo.json or HellionChat.yaml manifest fields? -- Does this affect the upstream cherry-pick path? See UPSTREAM_SYNC.md. +- Does this affect the upstream cherry-pick path? See docs/UPSTREAM_SYNC.md. --> ## Checklist @@ -67,6 +67,6 @@ new commands, new translations, removed behaviour. If none, write - [ ] I updated the README, in-plugin strings or documentation if my change is user-visible. - [ ] I did not include any AI-generated code without disclosing it - in the PR description (see [AI_DISCLOSURE.md](../AI_DISCLOSURE.md)). + in the PR description (see [AI_DISCLOSURE.md](../docs/AI_DISCLOSURE.md)). - [ ] I confirm my contribution is released under the [EUPL-1.2](../LICENSE). diff --git a/.github/release-footer.md b/.github/release-footer.md index 30ce250..1b98124 100644 --- a/.github/release-footer.md +++ b/.github/release-footer.md @@ -15,7 +15,7 @@ Dalamud main plugin repo. To install: - [README](https://github.com/JonKazama-Hellion/HellionChat/blob/main/README.md) — features, architecture, build - [Privacy notice](https://github.com/JonKazama-Hellion/HellionChat/blob/main/PRIVACY.md) — what the plugin stores and sends -- [Third-party notices](https://github.com/JonKazama-Hellion/HellionChat/blob/main/THIRD_PARTY_NOTICES.md) — dependencies and licences +- [Third-party notices](https://github.com/JonKazama-Hellion/HellionChat/blob/main/docs/THIRD_PARTY_NOTICES.md) — dependencies and licences - [Security policy](https://github.com/JonKazama-Hellion/HellionChat/blob/main/SECURITY.md) — vulnerability reporting - [Support](https://github.com/JonKazama-Hellion/HellionChat/blob/main/SUPPORT.md) — bug reports, questions, contact paths diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 80410db..60ba387 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ what I am not, and how to make a contribution land smoothly. - Read the [README](README.md) so you understand the scope: this is a privacy-focused, EUPL-1.2-licensed Dalamud plugin that intentionally removes the upstream webinterface and ships smaller defaults. -- Read [UPSTREAM_SYNC.md](UPSTREAM_SYNC.md). Cherry-picks from upstream +- Read [UPSTREAM_SYNC.md](docs/UPSTREAM_SYNC.md). Cherry-picks from upstream Chat 2 are selective and conscious; not everything that lands there belongs here. - Read [SECURITY.md](SECURITY.md). Anything security-sensitive goes @@ -22,7 +22,7 @@ what I am not, and how to make a contribution land smoothly. - Bug fixes for behaviour documented in the README, the in-plugin settings or the changelog. - Translation contributions for Hellion-specific strings via direct - pull requests against `ChatTwo/Resources/HellionStrings.*.resx`. + pull requests against `HellionChat/Resources/HellionStrings.*.resx`. Translations for the upstream Chat 2 strings (`Language.*.resx`) are not handled here; they go through the upstream Chat 2 project. - Documentation improvements (README, comments, this file). @@ -40,7 +40,7 @@ what I am not, and how to make a contribution land smoothly. They make selective upstream cherry-picks much harder and the maintenance cost outweighs the benefit for a one-person project. - AI-generated code dropped in without disclosure or human review. See - [AI_DISCLOSURE.md](AI_DISCLOSURE.md) for how I handle AI assistance + [AI_DISCLOSURE.md](docs/AI_DISCLOSURE.md) for how I handle AI assistance on my side; I expect comparable transparency from contributors. If you are unsure whether an idea fits, open a feature-request issue @@ -60,7 +60,7 @@ proposal than to a finished pull request. easier to review than one big one. Squash-on-merge happens at the PR level if needed. 5. If your change touches user-visible behaviour, update the README - and/or the changelog block in `ChatTwo/HellionChat.yaml` and + and/or the changelog block in `HellionChat/HellionChat.yaml` and `repo.json` for the next version. I bump the version number myself at release time, so you do not need to. 6. Open the pull request against `main`. The PR template will ask @@ -79,13 +79,12 @@ locally you need: ``` dotnet restore -dotnet build ChatTwo.sln -c Release -dotnet test ChatTwo.sln -c Release +dotnet build HellionChat.sln -c Release ``` -The test project is `ChatTwo.Tests`. New behaviour should come with a -test where the existing test infrastructure makes that practical -(privacy filter, configuration migration, message store). +Tests are not part of the current `HellionChat.sln`. If you add a test +project, point it at the relevant subsystems (privacy filter, +configuration migration, message store) and mention it in the PR. For a smoke test in-game: build, copy the output into your Dalamud `devPlugins/HellionChat/` directory and load it through `/xlplugins`. @@ -114,11 +113,11 @@ There is no separate CLA. ## Translations -Hellion-specific strings live in `ChatTwo/Resources/HellionStrings.resx` +Hellion-specific strings live in `HellionChat/Resources/HellionStrings.resx` (English source) and `HellionStrings..resx` (per-language). Translations are accepted as direct pull requests against those files. -The upstream Chat 2 strings in `ChatTwo/Resources/Language.*.resx` are +The upstream Chat 2 strings in `HellionChat/Resources/Language.*.resx` are **not** translated in this repository. They are owned by the upstream Chat 2 project and synced in via cherry-pick. Please contribute upstream-string translations to diff --git a/PRIVACY.md b/PRIVACY.md index dc9a197..a000250 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -112,7 +112,7 @@ on your behalf. and no requests to BetterTTV are made for the rest of the session. - **BetterTTV's privacy policy:** -Source: `ChatTwo/EmoteCache.cs`. +Source: `HellionChat/EmoteCache.cs`. ### 2. Square Enix Lodestone font (`img.finalfantasyxiv.com`) @@ -131,7 +131,7 @@ Source: `ChatTwo/EmoteCache.cs`. If a user-facing opt-out for this would be useful for you, please open a feature-request issue. -Source: `ChatTwo/FontManager.cs`. +Source: `HellionChat/FontManager.cs`. ### Links you click yourself (no automatic traffic) @@ -149,7 +149,7 @@ traffic. - **No telemetry.** Source verified: no calls to AppInsights, Sentry, PostHog, Plausible, Google Analytics, Microsoft Clarity or any comparable service exist in the codebase, nor in the direct - dependencies the plugin pulls in. See `THIRD_PARTY_NOTICES.md`. + dependencies the plugin pulls in. See `docs/THIRD_PARTY_NOTICES.md`. - **No crash reporting.** Crashes go to Dalamud's local `xllog`, not to a remote endpoint controlled by HellionChat. - **No usage counters.** The plugin does not count installs, sessions, @@ -222,7 +222,7 @@ or Dalamud, and BetterTTV is opt-out via settings. ## Dependencies that touch the network -For a full dependency inventory see `THIRD_PARTY_NOTICES.md`. Of the +For a full dependency inventory see `docs/THIRD_PARTY_NOTICES.md`. Of the direct dependencies the plugin pulls in: - `MessagePack` — local serialisation, no network. diff --git a/README.md b/README.md index b400060..8a6cb2b 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Hellion Chat ergänzt das ursprüngliche Chat-2-Fundament um Datenschutz- und Daten-Handling-Kontrollen, die mit den Datenschutz-Regeln in der EU, den USA und Japan im Einklang sind. Alle aus Chat 2 übernommenen Funktionen, Befehle und Tastenkürzel funktionieren unverändert. Eigenständiger Plugin-Slot, eigene Konfiguration, eigene Datenbank. -Eigenständiges Repository, EUPL-1.2-lizenziert. Distribution über Custom-Repo. Selektive Cherry-Picks von Upstream-Chat-2 nach Bedarf, dokumentiert in [UPSTREAM_SYNC.md](UPSTREAM_SYNC.md). +Eigenständiges Repository, EUPL-1.2-lizenziert. Mit v1.0.0 ist der Standalone-Cut abgeschlossen: eigener Namespace `HellionChat.*`, eigene IPC-Kanäle, eigene Source-Tree-Struktur. Distribution über Custom-Repo. Selektive Cherry-Picks von Upstream-Chat-2 nach Bedarf, dokumentiert in [`docs/UPSTREAM_SYNC.md`](docs/UPSTREAM_SYNC.md). ## Acknowledgements @@ -44,12 +44,12 @@ Hellion Chat baut auf [Chat 2](https://github.com/Infiziert90/ChatTwo) von **Inf - **Aufbewahrungsdauer pro Kanal** mit täglicher Background-Bereinigung. Tells 365 Tage, eigene Konversations-Kanäle 90 Tage, globaler Default 30 Tage. Standard ist AUS, das Plugin löscht ohne ausdrückliche Zustimmung nichts. - **Retroaktive Säuberung** mit Vorschau und Strg+Umschalt-Bestätigung. Wendet die aktuelle Whitelist auf eine bestehende Datenbank an, läuft im Hintergrund, ruft danach VACUUM auf. - **Export** nach Markdown, JSON oder CSV via Dalamud-Datei-Dialog (DSGVO Art. 15 Auskunftsrecht). Filter nach Kanal, Datums-Bereich oder Sender-Substring. -- **Vollständige Datenschutz-Übersicht** in [`PRIVACY.md`](PRIVACY.md): was gespeichert wird, welche zwei Outbound-Calls existieren (BetterTTV opt-out, Square-Enix-Lodestone-Font), explizite Telemetry-None-Zusage und das Mapping der DSGVO-Rechte (Art. 15/17/18/20/21) auf konkrete Plugin-Funktionen. +- **Vollständige Datenschutz-Übersicht** in [`PRIVACY.md`](PRIVACY.md) und Drittanbieter-Komponenten in [`docs/THIRD_PARTY_NOTICES.md`](docs/THIRD_PARTY_NOTICES.md): was gespeichert wird, welche zwei Outbound-Calls existieren (BetterTTV opt-out, Square-Enix-Lodestone-Font), explizite Telemetry-None-Zusage und das Mapping der DSGVO-Rechte (Art. 15/17/18/20/21) auf konkrete Plugin-Funktionen. ### Onboarding - **First-Run-Wizard** mit drei Profilen (Privacy-First, Locker, Volle Historie) und DSGVO-Hinweis bei der "Volle Historie"-Option. -- **Konfigurations-Migration v6→v7** seedet Privacy-Defaults bei Bestand-Usern und zeigt eine Benachrichtigung beim Ersten Plugin-Start nach Update. +- **Konfigurations-Migration** seedet Privacy-Defaults bei Bestands-Usern und zeigt eine Benachrichtigung beim ersten Plugin-Start nach Update. Mit v1.0.0 wird zusätzlich für User auf Config-Version 12 oder älter ein einmaliger Tab-Layout-Reset durchgeführt; die alte Tab-Konfiguration wird als `pluginConfigs/HellionChat.json.pre-v13-backup` gesichert. - **Layout-Migration aus Chat 2** verschiebt Konfiguration und Datenbank in `pluginConfigs/HellionChat/` ohne Datenverlust. Robust gegen blockierte Dateien (Warnung beim User wenn Chat 2 noch geladen ist). - **Migrate3-Recovery** heilt halb-migrierte Datenbanken aus alten Chat-2-Installationen. @@ -83,7 +83,7 @@ Hellion Chat baut auf [Chat 2](https://github.com/Infiziert90/ChatTwo) von **Inf ## Architektur ``` -ChatTwo/ +HellionChat/ ├── Privacy/ │ └── PrivacyDefaults.cs # Whitelist-Sets, Spec-Retention-Tabelle ├── Export/ @@ -92,6 +92,7 @@ ChatTwo/ │ ├── HellionStrings.resx # Hellion-eigene UI-Strings (EN) │ ├── HellionStrings.de.resx # Deutsche Übersetzung │ ├── HellionStrings.Designer.cs # Hand-maintained Accessor +│ ├── ChatColourPresets.cs # Sieben Built-in-Color-Presets (v0.6.0) │ ├── HellionFont.ttf # Exo 2 Variable Font │ ├── HellionFont-OFL.txt # OFL-1.1 Lizenztext (mit Font gebundelt) │ └── Language*.resx # Upstream-Lokalisierung (Crowdin) @@ -100,16 +101,19 @@ ChatTwo/ │ ├── HellionStyle.cs # ImGui-Theme-Push (lokal + global) │ └── SettingsTabs/ │ └── Privacy.cs # Datenschutz-Tab (Filter, Retention, Cleanup, Export) +├── Ipc/ # IPC-Kanäle, in v1.0.0 auf HellionChat.* migriert +├── ChatTwoConflictDetector.cs # Verweigert Plugin-Start wenn Upstream Chat 2 aktiv ├── images/ │ └── icon.png # Hellion-Logo (256×256) -├── DalamudPackager.targets # Override für ImagesPath / HandleImages +├── HellionChat.csproj # SDK Dalamud.NET.Sdk/15.0.0 └── HellionChat.yaml # Plugin-Manifest (DalamudPackager-Source) ``` ### Regeln -- **Code-Namespace ist `HellionChat.*`** — seit v1.0.0 vollständig konsolidiert auf den Plugin-Namen. -- **AssemblyName ist `HellionChat`** — eigener Slot in `pluginConfigs/`, eigene Datei-Manifest, kein Shared State mit Chat 2. +- **Code-Namespace ist `HellionChat.*`** — seit v1.0.0 vollständig konsolidiert auf den Plugin-Namen, kein verbleibender `ChatTwo.*`-Bestand im Source-Tree. +- **AssemblyName ist `HellionChat`** — eigener Slot in `pluginConfigs/`, eigenes Datei-Manifest, kein Shared State mit Chat 2. Parallel-Load mit Upstream Chat 2 wird beim Start aktiv geblockt (bilinguale Konflikt-Meldung). +- **IPC-Kanäle sind `HellionChat.*`** — sechs Kanäle für Drittplugin-Anbindung (`Register`, `Available`, `Unregister`, `Invoke`, `GetChatInputState`, `ChatInputStateChanged`). Details in [`docs/IPC.md`](docs/IPC.md). - **Hellion-eigene Strings in `HellionStrings.*.resx`**, übernommene Strings aus dem Chat-2-Bestand in `Language.*.resx` — die Original-`Language.*.resx` bleibt strukturell erhalten, weil die existierenden Übersetzungen aus dem Crowdin-Bestand der Upstream-Community weiter wertvoll sind. - **Kein Direkt-Eingriff in `Plugin.Interface.UiBuilder.FontAtlas`** außerhalb von `FontManager` — Font-Fallback und Hellion-Font laufen zentral. @@ -193,93 +197,50 @@ Updates erscheinen automatisch in der Plugin-Liste, sobald ein neuer `v0.X.Y`-Ta --- -## Entwicklung - -### Voraussetzungen - -- .NET 10 SDK (`10.0.104+`) und .NET 9 SDK (`9.0.115+` parallel) -- Dalamud-Hooks im XIVLauncher-`addon`-Verzeichnis -- VS Code mit C# Dev Kit (oder Rider, JetBrains) -- Linux: WireGuard-Mount für Test-Spiel-Setup falls Remote-DB - -### Setup - -```bash -git clone --recurse-submodules https://github.com/JonKazama-Hellion/HellionChat.git -cd HellionChat -git remote add upstream https://github.com/Infiziert90/ChatTwo.git - -# Linux: DALAMUD_HOME exportieren falls Hooks nicht im Standardpfad -cp .env.example .env -set -a; source .env; set +a - -dotnet build ChatTwo/ChatTwo.csproj -``` - -Output: `ChatTwo/bin/Debug/HellionChat.dll`. Den Ordner `ChatTwo/bin/Debug` in Dalamud unter Experimental → Dev Plugin Locations eintragen. - -### Build-Konfigurationen - -| Configuration | Output | Zweck | -| ------------- | ----------------------------------------------------- | -------------------------------- | -| Debug | `bin/Debug/HellionChat.dll` | Dev-Plugin-Loading | -| Release | `bin/Release/HellionChat/latest.zip` + Manifest | Custom-Repo / GitHub Release | - -### Upstream-Sync - -```bash -git fetch upstream -git log --oneline HEAD..upstream/main # Welche Commits gibt es? -git cherry-pick -x # Selektiv übernehmen -``` - -Konflikte in Upstream-Sprach-Ressourcen (`Language..resx`) kommen häufig vor, weil Upstream-Übersetzungen (über das Chat-2-Crowdin-Projekt, nicht unseres) regelmäßig nachkommen. Pragmatisch mit `git checkout --theirs` auflösen, da wir sie selbst nicht editieren. - ---- - ## Distribution -| Phase | Version | Distribution | -| --------------- | ------------- | -------------------------------------------------- | -| Bootstrap | v0.1.x | Eigenes Custom-Repo (`repo.json` im Repo-Root) | -| Stable | v1.0 | Eigenes Custom-Repo | -| Optional | v1.1+ | Submission ans Dalamud-Main-Plugin-Repo (zusätzlich) | +Hellion Chat wird über ein eigenes Dalamud-Custom-Repository verteilt +(`repo.json` im Repo-Root). Tag-Pushes auf `vX.Y.Z` lösen den +[`release.yml`](.github/workflows/release.yml)-Workflow aus, der den +Build-Output (`HellionChat/bin/Release/HellionChat/latest.zip`) plus den +passenden Changelog-Block aus `HellionChat.yaml` an das GitHub-Release +hängt. Manueller Recovery-Pfad bei verpasstem Auto-Trigger: +`gh workflow run release.yml -f tag=vX.Y.Z`. -`repo.json` wird beim Versions-Bump per Hand aus dem generierten `HellionChat.json` plus den GitHub-Release-Download-Links zusammengebaut. Skript-Automatisierung via GitHub Actions ist geplant aber noch nicht eingerichtet. +Eine optionale Submission ans Dalamud-Main-Plugin-Repo (zusätzlich zum +eigenen Custom-Repo) steht in der [Roadmap](docs/ROADMAP.md). --- ## Projektstatus -**Version 0.6.1** | Stand: 2026-05-03 +**Version 1.0.0** — Standalone-Cut live (Stand: 2026-05-04). -Alle Bootstrap-Phasen abgeschlossen: +Mit v1.0.0 ist Hellion Chat ein eigenständiges Plugin, kein Fork mehr im +Repository-Sinne. Vollständig abgeschlossen: -- [x] Privacy-Filter (Whitelist + Retention + Cleanup + Export) -- [x] First-Run-Wizard mit drei Profilen -- [x] Plugin-Identity (eigener Slot, Layout-Migration, Recovery) -- [x] Bilinguale UI (EN + DE) mit Live-Sprachwechsel -- [x] Hellion-Theme + Hellion-Logo + gebündelter Exo-2-Font -- [x] Custom-Repo-Pipeline mit GitHub-Release-Distribution -- [x] About-Tab im Hellion-Branding mit License + Disclaimer -- [x] AI-Disclosure dokumentiert (Pair-Klassifikation) -- [x] Webinterface entfernt (Phase 1.5, Audit-Konsequenz aus 2026-05-02) -- [x] Audit-Hardening Phase 2 (Path-Traversal, Retention-Race, DbViewer-Konsistenz, Privacy-Filter-Help-Text) -- [x] Slash-Commands auf `/hellion`-Familie umbenannt -- [x] Theme auf Hellion-Online-Media-Brand-Palette aligned (Arctic Cyan + Ember Orange) -- [x] About-Tab vollständig lokalisiert (EN + DE) mit Mission-Statement und neutraler Tonart +- Privacy-Filter (Whitelist, Retention, retroaktive Cleanup, Export) +- First-Run-Wizard mit drei Profilen +- Plugin-Identity: eigener `HellionChat`-Slot, Layout-Migration aus Chat 2, Migrate3-Recovery +- Bilinguale UI (EN + DE) mit Live-Sprachwechsel +- Hellion-Theme, Hellion-Logo, gebündelter Exo-2-Font +- Custom-Repo-Pipeline mit automatisierter GitHub-Release-Distribution +- Slash-Commands auf die `/hellion`-Familie konsolidiert +- Webinterface entfernt (v0.2.0) +- Audit-Hardening (Path-Traversal, Retention-Race, DbViewer-Konsistenz) +- About-Tab im Hellion-Branding, EN + DE lokalisiert, mit License und Disclaimer +- AI-Disclosure dokumentiert (siehe [`docs/AI_DISCLOSURE.md`](docs/AI_DISCLOSURE.md)) +- Standalone-Cut: Namespace `HellionChat.*`, IPC-Kanäle `HellionChat.*`, Source-Tree-Restructure, Conflict-Detection gegen Upstream Chat 2, SQLite-CVE-Härtung (3.50.3) -Phase 3 (offen, kein festes Datum): +Was als Nächstes geplant ist und welche Themen langfristig auf der Liste +stehen, steht in [`docs/ROADMAP.md`](docs/ROADMAP.md). Konkrete +eingeplante Items werden zusätzlich im +[GitHub-Issue-Tracker](https://github.com/JonKazama-Hellion/HellionChat/issues) +mit dem `roadmap`-Label geführt. -- [ ] MySQL/MariaDB-Backend mit Drei-Stufen-Bestätigung -- [ ] PostgreSQL-Backend -- [ ] Encryption für sensible Channels (AES-256, lokaler Key) -- [ ] WireGuard-Network-Detection (optionaler Filter) -- [ ] libnotify-Integration (native Linux-Toasts) -- [ ] XDG-Compliance (komplex unter Wine) -- [ ] Hand-gezeichnetes Hellion-Logo (Platzhalter aus Hellion-Online-Media-Brand-Repo) -- [ ] GitHub-Actions für reproduzierbaren Build und automatischen `repo.json`-Sync -- [ ] Submission ans Dalamud-Main-Plugin-Repo +### Zur Release-Kadenz + +Wer den Repo zum ersten Mal sieht, bemerkt schnell viele Releases und sehr viele Commits in kurzer Zeit. Beides ist eine bewusste Entscheidung, keine KI-Slop-Symptomatik: Vorarbeit vor dem Fork (Issues und Commits gelesen, Chat 2 ingame genutzt), eine sauber strukturierte Upstream-Codebase als Fundament, atomare Commits im Stil des Upstream und AI-gestütztes Review-Sparring, das ich nicht blind übernehme. Die volle Begründung steht in [`docs/LEARNING-JOURNEY.md`](docs/LEARNING-JOURNEY.md), Sektion "Wie ich so schnell release". --- @@ -310,23 +271,40 @@ FINAL FANTASY XIV © SQUARE ENIX CO., LTD. Alle Rechte vorbehalten. Hellion Chat ### KI-Unterstützung -Siehe [`AI_DISCLOSURE.md`](AI_DISCLOSURE.md) für die Pair-Level-Disclosure. +Siehe [`docs/AI_DISCLOSURE.md`](docs/AI_DISCLOSURE.md) für die Pair-Level-Disclosure. --- ## Projekt-Dokumente +Im Repo-Root liegen die Standard-Repository-Dokumente, vertiefende +Dokumentation lebt unter [`docs/`](docs/). + +### Repo-Root + | Dokument | Inhalt | | --- | --- | | [`PRIVACY.md`](PRIVACY.md) | Datenschutz-Übersicht: lokale Speicherung, Outbound-Calls, Telemetry-Status, DSGVO-Rechte und ihre Plugin-Entsprechungen. | | [`SECURITY.md`](SECURITY.md) | Vulnerability-Reporting via Private Advisory, Scope und Disclosure-Fenster. | -| [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md) | NuGet-Dependencies mit Lizenzen, Bundled Assets, Network-Status pro Komponente. | -| [`CONTRIBUTING.md`](CONTRIBUTING.md) | Was ich akzeptiere bzw. ablehne, Workflow, Build-Anleitung, EUPL-1.2-Bestätigung. | -| [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) | Verhaltens-Erwartungen und Reporting-Pfad. | | [`SUPPORT.md`](SUPPORT.md) | Wegweiser für Bugs, Security, Privacy, Quick-Questions. | -| [`UPSTREAM_SYNC.md`](UPSTREAM_SYNC.md) | Cherry-Pick-Policy gegenüber Chat 2. | +| [`CONTRIBUTING.md`](CONTRIBUTING.md) | Was ich akzeptiere bzw. ablehne, Workflow, EUPL-1.2-Bestätigung. | +| [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) | Verhaltens-Erwartungen und Reporting-Pfad. | | [`NOTICE.md`](NOTICE.md) | Attribution an Upstream-Maintainer und Komponenten-Credits. | -| [`AI_DISCLOSURE.md`](AI_DISCLOSURE.md) | Offenlegung der KI-Unterstützung im Entwicklungsprozess. | +| [`COPYRIGHT`](COPYRIGHT) | Copyright-Notes mit Dual-Holder-Block. | +| [`LICENSE`](LICENSE) | EUPL-1.2 Volltext. | + +### `docs/` + +| Dokument | Inhalt | +| --- | --- | +| [`docs/ROADMAP.md`](docs/ROADMAP.md) | Geplante Cycles, mittelfristige und langfristige Themen. | +| [`docs/CHANGELOG.md`](docs/CHANGELOG.md) | Kuratierte Versions-Übersicht mit Verweis auf die GitHub-Release-Pages. | +| [`docs/CONTRIBUTORS.md`](docs/CONTRIBUTORS.md) | Tester, Übersetzer und Code-Beiträger der Hellion-Seite. | +| [`docs/LEARNING-JOURNEY.md`](docs/LEARNING-JOURNEY.md) | Entwicklungsgeschichte, vom Web-Stack zu C# / Dalamud, was ich aus dem Fork gelernt habe. | +| [`docs/IPC.md`](docs/IPC.md) | IPC-Kanal-Reference, Tuple-Payload-Felder, Migrations-Diff für Drittplugins. | +| [`docs/UPSTREAM_SYNC.md`](docs/UPSTREAM_SYNC.md) | Cherry-Pick-Policy gegenüber Chat 2. | +| [`docs/THIRD_PARTY_NOTICES.md`](docs/THIRD_PARTY_NOTICES.md) | NuGet-Dependencies mit Lizenzen, Bundled Assets, Network-Status pro Komponente. | +| [`docs/AI_DISCLOSURE.md`](docs/AI_DISCLOSURE.md) | Offenlegung der KI-Unterstützung im Entwicklungsprozess. | --- diff --git a/AI_DISCLOSURE.md b/docs/AI_DISCLOSURE.md similarity index 97% rename from AI_DISCLOSURE.md rename to docs/AI_DISCLOSURE.md index c37ba10..a9065da 100644 --- a/AI_DISCLOSURE.md +++ b/docs/AI_DISCLOSURE.md @@ -49,7 +49,7 @@ comfortable with Dalamud and plugin development in general. Upstream Chat 2 (by Infi & Anna, EUPL-1.2) is the foundation and was not produced with AI assistance. Hellion-specific code lives in -`ChatTwo/Privacy/`, `ChatTwo/Export/`, `Resources/HellionStrings*`, +`HellionChat/Privacy/`, `HellionChat/Export/`, `HellionChat/Resources/HellionStrings*`, `Ui/SettingsTabs/Privacy.cs`, `Ui/FirstRunWizard.cs`, `Ui/HellionStyle.cs`, plus the Migrate3 recovery and plugin layout migration in `MessageStore.cs` and `Plugin.cs`. These were developed with Pair-level assistance as diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md new file mode 100644 index 0000000..9950f89 --- /dev/null +++ b/docs/CHANGELOG.md @@ -0,0 +1,83 @@ +# Changelog — Hellion Chat + +Alle nutzersichtbaren Änderungen an Hellion Chat. Das Format orientiert +sich an [Keep a Changelog](https://keepachangelog.com/de/1.0.0/), die +Version-Nummern folgen [Semantischer Versionierung](https://semver.org/lang/de/). + +Detaillierte Release-Notes pro Version stehen direkt am +[GitHub-Release](https://github.com/JonKazama-Hellion/HellionChat/releases) +und im Plugin-Changelog-Block (`HellionChat/HellionChat.yaml` → +`changelog:`). Diese Datei fasst die Releases als Überblick zusammen +und verlinkt für Details auf die Release-Pages. + +--- + +## [1.0.0] — 2026-05-03 — Standalone Major Release + +Erste vollständig eigenständige Version. Code-Namespace, IPC-Kanäle und +Source-Tree-Struktur wurden auf `HellionChat.*` konsolidiert. Plugin +verweigert den Start bei aktivem Upstream Chat 2 (bilinguale +Konflikt-Meldung). SQLite-Native auf 3.50.3 gepinnt (CVE-2025-6965, +CVE-2025-7709). Tab-Layout-Default für neue Installationen und für +User auf Config-Version 12 oder älter neu strukturiert (5 thematische +Tabs statt 6+ kitchen-sink). Sweep aus Critical- und Major-Findings +aus dem Codebase-Audit eingearbeitet. + +[Release-Notes 1.0.0](https://github.com/JonKazama-Hellion/HellionChat/releases/tag/v1.0.0) + +## [0.6.1] — 2026-05-03 — Pop-Out Discoverability & /tell Auto-Pop-Out + +Pop-Out-Button im Chat-Header sichtbar, einmaliger Hint-Banner für die +Pop-Out-Funktionalität. Neue Einstellung "Neue /tell-Tabs direkt als +Pop-Out öffnen". Pop-Out-Input ist jetzt standardmäßig aktiv. +Bugfixes: Ghost-Windows bei LRU-Drop / Logout, Dead-Zone unter dem +Input-Bar bei aktivem Hint-Banner. + +[Release-Notes 0.6.1](https://github.com/JonKazama-Hellion/HellionChat/releases/tag/v0.6.1) + +## [0.6.0] — 2026-05-03 — UX Polish: Pop-Out Input + Colour Presets + +Zwei opt-in UX-Features. Pop-Out-Fenster bekommen optional eine +kompakte Eingabe-Bar mit channel-farbigem Icon-Button und unabhängigem +Text-Buffer pro Pop-Out. Sieben Built-in-Color-Presets (Klassik, +High-Contrast, Pastell, Dark-Mode-Tuned, Hellion, Night Blue, Indigo +Violet) zum One-Click-Apply. Konfigurations-Migration v10 → v11. + +[Release-Notes 0.6.0](https://github.com/JonKazama-Hellion/HellionChat/releases/tag/v0.6.0) + +## [0.5.4] — 2026-05-02 — WrapText Hardening + +`ImGuiUtil.WrapText` von Pointer-Arithmetik auf Span- und +Index-basierten Control-Flow umgestellt. Schließt das wiederkehrende +CodeQL-Critical-Alert "unvalidated local pointer arithmetic" +dauerhaft. Keine nutzersichtbare Verhaltensänderung — Word-Wrap-Output +ist byte-identisch zu 0.5.3. + +[Release-Notes 0.5.4](https://github.com/JonKazama-Hellion/HellionChat/releases/tag/v0.5.4) + +## [0.5.3] — 2026-05-02 — Pointer Arithmetic Hardening + +Erster Anlauf zur Schließung des CodeQL-Critical-Alerts in +`ImGuiUtil.WrapText`. Encoded-Byte-Buffer-Length wird vor der +Pointer-Arithmetik via `GetByteCount` validiert. + +[Release-Notes 0.5.3](https://github.com/JonKazama-Hellion/HellionChat/releases/tag/v0.5.3) + +--- + +## Frühere Versionen + +Releases vor 0.5.3 (Bootstrap-Phase 0.1.0 bis 0.5.2) sind direkt am +GitHub-Release-Stream einsehbar: + +[Alle Releases](https://github.com/JonKazama-Hellion/HellionChat/releases) + +--- + +## Pflege-Hinweis + +Die Source-of-Truth für den nutzersichtbaren Changelog ist der +`changelog:`-Block in `HellionChat/HellionChat.yaml`. `repo.json` und +der GitHub-Release-Body werden daraus gespeist. Diese Datei +(`docs/CHANGELOG.md`) ist eine kuratierte Zusammenfassung mit Verweis +auf die Release-Pages und wird beim Versions-Bump manuell ergänzt. diff --git a/docs/CONTRIBUTORS.md b/docs/CONTRIBUTORS.md new file mode 100644 index 0000000..30499e7 --- /dev/null +++ b/docs/CONTRIBUTORS.md @@ -0,0 +1,61 @@ +# Contributors — Hellion Chat + +Hellion Chat ist von der Code-Seite ein Ein-Personen-Projekt. Aber ohne die Leute auf dieser Seite gäbe es weder die Bug-Fixes noch die UX-Verbesserungen, die seit den frühen Versionen reingelaufen sind. Jeder Eintrag hier hat das Plugin konkret besser gemacht. + +Die Anerkennung an die Upstream-Autoren von Chat 2 (Infi und Anna) liegt bewusst in [`../NOTICE.md`](../NOTICE.md), nicht hier. Diese Datei deckt explizit Beiträge zur Hellion-Chat-Seite ab. + +--- + +## Entwicklung + +### JonKazama (Florian Wathling) — Maintainer + +Hellion Chat ist mein erstes FFXIV-Plugin und mein erstes größeres C#-/Dalamud-Projekt. Mein beruflicher Hintergrund ist Webentwicklung (Next.js, React, TypeScript, Prisma). Plugin-Entwicklung in einer fremden Codebase, ImGui, FFXIV-Game-Hooks und der gesamte Dalamud-Stack waren Neuland. + +Privacy-First-Defaults, Per-Channel-Retention, Auto-Tell-Tabs, Pop-Out-Input, ChatColours-Presets, Hellion-Theme plus Exo-2-Font und der v1.0.0-Standalone-Cut sind die Hellion-spezifischen Surface-Areas, die ich auf das Chat-2-Fundament aufgebaut habe. Die Lern-Geschichte dahinter steht in [`LEARNING-JOURNEY.md`](LEARNING-JOURNEY.md). + +Hellion Chat ist Teil von [Hellion Online Media](https://hellion-media.de). + +--- + +## Tester + +Eine kurze Notiz vorneweg: Ich teste das Plugin nicht allein. Die Leute hier haben mir Bugs gemeldet, bevor sie bei mehr Nutzern aufgeschlagen wären. Sie haben UX-Probleme angesprochen, die ich blind nicht mehr gesehen habe. Und sie haben Feature-Wünsche eingebracht, die das Plugin in Richtungen geschoben haben, in die ich von alleine nicht gegangen wäre. Das ist nicht selbstverständlich. Externe Tester sind ihre Zeit wert. + +### Carl Beleandis (Carla) — Beta-Tester + +Carl testet seit der Bootstrap-Phase und hat sowohl die Pop-Out-Mechanik als auch die Theme-Richtung geprägt. Sein Feedback kommt direkt und ohne Umschweife und das ist genau, was ich beim Testen brauche. + +Konkrete Beiträge: + +- **Pop-Out-Discoverability** — der Hinweis, dass Pop-Outs nur per Rechtsklick erreichbar waren, hat den Header-Button und den einmaligen Hint-Banner in v0.6.1 ausgelöst. Ich kannte den Rechtsklick-Pfad blind, deshalb hatte ich nicht mehr gesehen, dass neue Nutzer die Funktion gar nicht finden. +- **/tell-Pop-Out-Mode** — der Wunsch, /tell-Tabs direkt als Pop-Out zu öffnen statt über den Tab-Umweg, ist in v0.6.1 als opt-in Settings-Toggle gelandet. Bonus: Bei der Implementation ist ein alter Ghost-Window-Bug aufgefallen (LRU-Drop ließ Pop-Out-Fenster als Geister stehen), der gleich mit gefixt wurde. +- **Theme-Varianten mit Helligkeits-Abstufungen** — der Wunsch nach einer Grün-Familie hat mein Verständnis von "ein Theme = eine Farbe" auf "Theme-Familien mit Stimmungs-Varianten" verschoben. Steht in der [Roadmap](ROADMAP.md) für einen späteren Cycle. + +### Jin (Jingliu) — Alpha-Tester + +Jin ist der aktive Tester der ersten Stunde und hat den Pop-Out-Workflow architektonisch in eine andere Richtung geschoben. + +Konkrete Beiträge: + +- **Pop-Out-Tab mit Input-Feld** — der Vorschlag, in einem Pop-Out auch tippen zu können (statt nur lesen), hat die v0.6.0 Pop-Out-Input-Bar ausgelöst. Das war ein größerer Refactor: Der Input-Layer aus `ChatLogWindow` musste so geöffnet werden, dass er auch in `Popout.cs` lebt, mit unabhängigem Text-Buffer und History-Cursor pro Pop-Out. Hat den Cycle dominiert, weil das Design erst sauber sein musste, bevor Code passieren konnte. +- **TempTell Persistence** — der Wunsch, /tell-Tabs per Pin-Toggle einen Relog überleben zu lassen, steht in der [Roadmap](ROADMAP.md) für einen späteren Cycle. Berührt das Tab-System architektonisch und braucht eigenes Design. + +--- + +## Übersetzungen + +Hellion-eigene UI-Strings werden in `HellionChat/Resources/HellionStrings..resx` gepflegt. + +- **Deutsch (DE):** JonKazama (Native Speaker, Hauptsprache des Projekts) + +Die Upstream-Sprach-Dateien (`Language..resx`) sind nicht Teil dieser Datei. Sie werden über das [Chat-2-Crowdin-Projekt](https://github.com/Infiziert90/ChatTwo) gepflegt; Crowdin-Übersetzer findest du in den Plugin-Settings unter **Info → "Chat 2 community translators"**. + +--- + +## Wie du beitragen kannst + +Bug-Reports, Feature-Wünsche und Pull-Requests laufen über [GitHub Issues](https://github.com/JonKazama-Hellion/HellionChat/issues). Workflow und Erwartungen stehen in [`../CONTRIBUTING.md`](../CONTRIBUTING.md), Code of Conduct in [`../CODE_OF_CONDUCT.md`](../CODE_OF_CONDUCT.md). + +Tester-Pool für neue Versionen läuft über den Hellion-Forge-Discord: [discord.gg/X9V7Kcv5gR](https://discord.gg/X9V7Kcv5gR). Wer in den Tester-Channel rein will, einfach im Forge melden. + diff --git a/ipc.md b/docs/IPC.md similarity index 100% rename from ipc.md rename to docs/IPC.md diff --git a/docs/LEARNING-JOURNEY.md b/docs/LEARNING-JOURNEY.md new file mode 100644 index 0000000..e5954e4 --- /dev/null +++ b/docs/LEARNING-JOURNEY.md @@ -0,0 +1,219 @@ +# Entwicklungsgeschichte und Lernprozess + +## Hintergrund + +Ich bin Autodidakt. Hellion Chat ist mein erstes FFXIV-Plugin und mein erstes größeres C#-Projekt. Mein beruflicher Hintergrund ist Webentwicklung (Next.js, React, TypeScript, Prisma, MySQL), also Browser-Welt mit JavaScript-Toolchain. C# kannte ich vor diesem Projekt nur oberflächlich, ImGui gar nicht, Dalamud nur als Endnutzer über andere Plugins. + +Wenn ich an einer Stelle nicht weiterkomme, nutze ich AI-Tools wie Claude Code als Pair-Hilfsmittel. Wie das genau aussieht und welche Klassifikation ich verwende, steht transparent in [`AI_DISCLOSURE.md`](AI_DISCLOSURE.md). + +--- + +## Warum überhaupt ein Chat-Plugin? + +Hellion Chat soll Chat 2 nicht ersetzen. Chat 2 liefert ein vollständiges Chat-Erlebnis mit kompletter Historie, Filtern, Suche und Replay. Für die meisten Nutzer ist genau das richtig. + +### Zwei Millionen Nachrichten in zwei Jahren + +Mein Wunsch nach einem engeren Default war ehrlich gesagt erstmal persönlich. Nach zwei Jahren mit Chat 2 lag meine Datenbank bei über zwei Millionen Nachrichten, der Großteil davon /say, /shout und /yell von wildfremden Leuten in Limsa. Genau diese Daten machen Chat 2's Voll-Historie nützlich, und die meisten Nutzer behalten sie auch gerne. Mein eigener Geschmack wollte einen kleineren Default. Also habe ich diesen Fork gebaut. + +### Greeter in mehreren Clubs + +Dazu kam ein zweiter Use-Case: Ich bin in mehreren FFXIV-Clubs als Greeter aktiv. Für die Greeter-Arbeit reicht die Vanilla-Chat-Oberfläche nicht. Parallel laufende /tell-Gespräche schreiben in einem einzigen Tab durcheinander, und ich verliere ständig den Faden, wer mir gerade was geschrieben hat. Auto-Tell-Tabs (eines der frühen Hellion-Chat-Features) ist genau für diesen Workflow entstanden: ein Tab pro Gesprächspartner, automatisch gespawnt, mit manuellem Greeted-Status. Dass das auch der Privacy-Hygiene gut tut, war ein netter Bonus, nicht der Auslöser. + +### Hellion Online Media + +Die Privacy-Defaults sind außerdem eine Position aus meinem Hauptberuf. Hellion Online Media ist mein Einzelunternehmen, und Datenschutz gegenüber Kunden ist da kein Marketing-Slogan, sondern operativ relevant. Dieser Fork ist die Plugin-Form derselben Haltung. + +--- + +## Warum nicht beim Original mitarbeiten? + +Drei Gründe, in absteigender Wichtigkeit. + +### Defaults sind nicht verhandelbar, auch nicht meine + +Privacy-First als Standard ist eine Minderheits-Position. Chat 2 bedient zu Recht die breite Masse mit Voll-Historie als Default. Diese Defaults im Upstream zu ändern wäre falsch gewesen. Ich hätte den Standard für eine große Nutzerbasis umgekippt, die ihn so wollte, wie er ist. Saubere Trennung über einen eigenen Plugin-Slot war der respektvollere Weg. + +### Das Webinterface musste weg + +Das ist ein zentrales Chat-2-Feature für Remote-Zugriff vom Zweitgerät. Ein PR der das entfernt, hat in einem gepflegten Upstream-Projekt keine Chance, und das ist auch richtig so. Aber genau das Webinterface kollidiert mit der Privacy-First-These dieses Forks: Ein Chat-Plugin das einen lokalen HTTP-Server startet, ist für mein Threat-Model eine zu große Angriffsfläche. Also raus damit. + +### Tempo + +Ein Solo-Maintainer-Projekt mit kleinem Tester-Pool kann schneller iterieren als ein etabliertes Plugin mit großer Nutzerbasis. Das ist kein Vorwurf an Upstream, sondern eine andere Optimierung. Ich brauche keine Roadmap-Abstimmung, keine Reviewer-Verfügbarkeit, und kann Audit-Konsequenzen wie das Webinterface-Removal in einer einzigen Version durchziehen statt über mehrere Releases. + +EUPL-1.2 erlaubt das alles ausdrücklich, mit klarer Attribution. Der Code liegt offen unter derselben Lizenz wie Chat 2. Infi, Anna oder sonst jemand dürfen reinschauen, Ideen mitnehmen, Fragen stellen oder den Fork einfach ignorieren. Alles drei ist für mich okay. + +--- + +## Wie ich so schnell release + +Wer auf den Repo schaut, sieht in kurzer Zeit viele Releases und sehr viele Commits. Beides wird von außen gerne als Red-Flag gelesen: KI-Slop, Salami-Taktik, Code-Spam. Bei Hellion Chat ist beides eine bewusste Entscheidung, und ich erkläre lieber einmal warum, als mich später dafür zu rechtfertigen. + +### Vorarbeit, lange bevor der Fork existierte + +Bevor ich die erste Zeile in `HellionChat/` getippt habe, war ich wochenlang nur Leser. Chat 2 ingame nutzen und damit rumspielen. Issues im Upstream-Tracker durchgehen, vor allem die geschlossenen, weil dort steht, wie Infi und Anna Bugs einkreisen. Commits lesen, gerne auch ältere, um zu verstehen, warum eine Architektur-Entscheidung getroffen wurde, nicht nur, dass sie getroffen wurde. Wenn ich heute weiß, wo im Code was liegt, dann nicht, weil ich besonders schnell durch eine Codebase navigiere, sondern weil ich den Code vorher gelesen habe. + +Klingt nach Selbstverständlichkeit, ist es aber nicht. Die übliche Reihenfolge bei Solo-Forks heißt erst forken, dann verstehen. Ich habe es andersrum gemacht. + +### Die Codebase von Infi und Anna + +Hellion Chat baut auf einem Boden auf, der schon flach ist. Chat 2 ist sauber strukturiert, die Naming-Konventionen sind konsistent, die Trennung zwischen Layern (Storage, UI, Game-Hooks, IPC) ist klar gezogen. Das ist in Open-Source-Plugin-Welten nicht selbstverständlich, und es ist der Hauptgrund, warum sich Hellion-spezifische Features oft "fast nativ" einbauen lassen. Ich muss nicht erst Spaghetti entwirren bevor ich was Eigenes danebenstellen kann. + +Side-Fact: Selbst beim ersten Codebase-Walkthrough mit Claude kam mehrfach der Hinweis, dass die Architektur ungewöhnlich gut aufgeräumt ist und mehrere Erweiterungspunkte vorbereitet. Das hat Gewicht, weil es von außen kommt, aber den eigentlichen Kredit kriegen Infi und Anna, nicht Claude. + +### Atomar arbeiten, kleine Commits + +Ein Commit, eine logische Änderung. Wenn ich einen Bug fixe, parallel eine Variable umbenenne und nebenbei einen Kommentar einbaue, sind das drei Commits, nicht einer. Klingt nach Mikro-Management, ist es aber nicht. Wenn in sechs Monaten ein Bug auftaucht und ich `git bisect` brauche, finde ich die kaputte Änderung in zwei Minuten statt in zwei Stunden. Bei einem 4000-Zeilen-Mega-Commit darf ich raten, welche der hundert Änderungen die kaputte ist. + +Den Stil habe ich bewusst auch deshalb beibehalten, weil Infi im Upstream häufig genauso arbeitet. Manchmal ein Sechs-Zeilen-Commit, manchmal nur ein Typo-Fix. Das ist keine Schwäche, das ist eine Entscheidung für lesbare Git-History. Den Stil im Fork beizubehalten ist ein Respekt-Move: Wer die beiden Repos vergleicht, soll den gleichen Lese-Rhythmus haben. + +Bonus für mich persönlich: Kleine Commits zwingen mich, jeden Schritt einzeln zu durchdenken und zu benennen. Wenn ich nicht in zwei Sätzen erklären kann, was ein Commit macht, ist die Änderung wahrscheinlich noch nicht klar genug. Auf Beginner-Niveau ist das ein eingebauter Sanity-Check, den ich bei einem Big-Bang-Commit nicht hätte. + +### AI als Beschleuniger, ehrlich + +Ja, AI hilft beim Tempo, und nicht zu knapp. Ohne CodeRabbit hätte ich Critical-Bugs der Klasse `Equals/GetHashCode`-Anti-Pattern, Hook-Subscription-Leaks und TOCTOU-Races nicht gefunden. Ich bin schlicht zu unerfahren für diese Klasse von Findings, das schreibe ich genau so hin. + +Was ich aber nicht mache: blind Code übernehmen, weil ein Tool ihn als Fix markiert hat. Bei mehreren CodeRabbit-Findings stand in den Original-Commits von Infi oder Anna sogar ein Stackoverflow-Link mit Begründung dabei, warum eine bestimmte Stelle so aussieht wie sie aussieht. Die habe ich gelesen, bevor ich was geändert habe. Erst verstehen, dann anfassen, dann committen. Das ist der Unterschied zwischen "AI gibt mir Code, ich pushe" und "AI zeigt mir wo's klemmt, ich entscheide". + +Klassifikation und konkrete Beispiele zur AI-Nutzung stehen in [`AI_DISCLOSURE.md`](AI_DISCLOSURE.md). Hier in dieser Sektion ging es nur um den Tempo-Aspekt: Recherche plus saubere Codebase plus atomare Commits plus AI-gestütztes Review-Sparring sind die vier Faktoren zusammen. Kein einzelner davon erklärt das Tempo allein. + +--- + +## Vom Web-Stack zu C# / Dalamud + +### Type-System? Weniger Schock als erwartet + +C# nach TypeScript war angenehmer als gedacht. Properties statt getter/setter sind sauber, nullable reference types fühlen sich an wie `strict: true` in TypeScript. Ungewohnt war Wert-Typen vs. Referenz-Typen explizit denken zu müssen (`struct` vs. `class` mit echten Verhaltens-Konsequenzen), und Generics mit Constraints sind syntaktisch anders genug, dass ich beim Lesen kurz stocke. `async`/`await` ist semantisch ähnlich, aber Threading-Modelle sind in C# expliziter: `Task.Run`, `ConfigureAwait`, Synchronization-Contexts. Das hat mich mehrere Bugs gekostet, bevor ich verstanden hatte, wann der Main-Thread (in Plugin-Welt: der Framework-Tick) wirklich kritisch ist. + +### Build-Toolchain: ähnlich, aber anders + +`dotnet` CLI, csproj-XML, NuGet sind funktional nicht weit weg von npm und tsconfig. Aber das XML-Format der csproj ist eine andere Sprache als JSON-Configs. Die Lock-Datei (`packages.lock.json`) musste ich erst aktiv aktivieren (`RestorePackagesWithLockFile=true`); das ist nicht Default. Im Web-Stack ist Lock-File-First Standard, im .NET-Stack offenbar nicht. Das war eine echte Überraschung. + +### ImGui ist eine andere Welt + +Immediate-Mode-Rendering hat mit React-Component-Trees nichts gemein. Es gibt keine virtuelle DOM, keine Reconciliation, keinen "State der Komponente". Pro Frame zeichnet der Code die UI komplett neu, und der State lebt entweder in lokalen Variablen, die ich selbst verwalten muss, oder in der ImGui-eigenen ID-Stack-Logik. + +Was in React zwei Zeilen `useState` sind, ist in ImGui ein Member-Field plus manuelle ID-Stempel auf den Widgets, sonst kollidieren zwei Selectables in derselben Loop, weil sie auf die gleiche ID zurückfallen. Die ID-Stack-Kollision in `SearchSelector` (gefixt in v1.0.0) war genau dieses Symptom: Alle Selectables fielen auf dieselbe ambiguous ID zurück, bis ich den Row-Index in den Push-ID gemixt habe. Klassischer "warum klickt der falsche Eintrag"-Bug, den man nur findet, wenn man verstanden hat, wie ImGui IDs intern handhabt. + +### Dalamud-Spezifika + +Plugin-Lifecycle, IPC-Subscriber-Pattern, Hook-System für Game-Functions, Game-Object-Threading. Viel davon war nur durch Lesen der Upstream-Codebase und durch [dalamud.dev](https://dalamud.dev) zu verstehen. Meine Trainings- und Such-Ergebnisse für "Dalamud" liefern oft veraltete API-Beispiele aus alten Versionen. dalamud.dev ist die zuverlässige Quelle. Wenn jemand neu anfängt: dort hin, nicht zu Stack Overflow. + +### Der Tag, an dem mich der DalamudPackager einen Tag gekostet hat + +Dalamud SDK 15 liefert seinen eigenen Default-Packager mit, der Icons und Image-URLs ins Manifest einträgt. Ich hatte aus dem Upstream-Repo eine eigene `DalamudPackager.targets`-Datei mit `HandleImages`-Override übernommen, und die hat den SDK-Default überschrieben. Resultat: Das Manifest hatte keinen `IconUrl` mehr, und das Plugin tauchte in der Plugin-Liste ohne Icon auf. + +Symptom war einfach zu sehen, Ursache hat einen Tag gekostet. Ich hatte die Override-Datei für eine Pflicht-Datei gehalten, war sie aber nicht. Removal in v0.5.2, seitdem läuft der SDK-Default. Lektion: Erstmal mit Defaults arbeiten, Overrides erst wenn der Default nachweislich nicht passt. + +--- + +## Was ich aus dem Fork gelernt habe + +### Refactor in einer fremden Codebase + +Der Standalone-Cut in v1.0.0 hat die `ChatTwo.*`-Identität komplett auf `HellionChat.*` migriert. Klingt nach Find-and-Replace. War es nicht. + +Konkret bedeutete das: Code-Namespace über alle 80 Source-Files plus 100 using-Direktiven plus zwei FQN-Aliases plus die Resource-Designer-Strings. Sechs IPC-Channels umbenannt (Breaking Change für Drittplugins, keine bekannten Anbindungen). Repo-Ordner-Struktur (`ChatTwo/` → `HellionChat/`) inklusive csproj, sln, allen GitHub-Workflows und der dependabot.yml. Public-Facing-Branding in README, repo.json, yaml auf Standalone-Framing umformuliert. + +Das war kein Solo-Find-and-Replace, weil Unicode-String-Pfade in Workflow-YAMLs anders quotiert werden müssen als C#-Strings. Weil Resource-Designer-Files generierte Inhalte haben, die nicht jede Toolchain im Blick hat. Und weil die `ChatTwo.*`-IPC-Channel-Namen Strings in `GetIpcSubscriber`-Calls sind: kein Symbol, kein Compile-Error, wenn man einen vergisst. Da merkst du, was alles still bleibt. + +### Sicherheit ist kein abstraktes Thema mehr + +Vor diesem Projekt war Supply-Chain-Sicherheit für mich akademisch. Drei konkrete Lektionen haben das geändert. + +**SQLite-Native-Binary.** Ich musste auf 3.50.3 pinnen (`SQLitePCLRaw.lib.e_sqlite3` Override), weil `Microsoft.Data.Sqlite` die transitiv nachgezogene Lib in einer Version mitschleppte, die CVE-2025-6965 (Memory-Corruption durch Aggregate-Term-Overflow) und CVE-2025-7709 enthielt. Der Managed-Wrapper war neu, die Native-Lib war es nicht. Lektion: Transitive Dependencies prüfen sich nicht von selbst, du musst hinschauen. + +**Lock-File-Drift.** `packages.lock.json` honored bei `dotnet restore` (per `RestorePackagesWithLockFile=true` in der csproj) verhindert, dass transitive Versionen zwischen meiner Maschine und CI silent driften. Erst nach einem Build-Output-Mismatch zwischen lokal und GitHub-Actions hatte ich überhaupt verstanden, warum das nicht der Default ist. + +**WrapText und der CodeQL-Alarm der drei Releases gekostet hat.** CodeQL hat in `ImGuiUtil.WrapText` einen Critical-Alert wegen "unvalidated local pointer arithmetic" geworfen. v0.5.2 hat einen Edge-Case validiert. Alert kam wieder. v0.5.3 hat den Buffer-Length via `GetByteCount` vor der Pointer-Math gecheckt. Alert kam wieder. v0.5.4 hat den ganzen Algorithmus auf `Span` und int-Offsets umgebaut, mit einem 16-KiB-Cap auf den ArrayPool-Rent. Erst da war Ruhe. + +Lektion: Wenn ein statischer Analyzer drei Mal hintereinander meckert, ist nicht der Analyzer überempfindlich. Die Datenflusslogik ist es. + +### CodeRabbit als externer Code-Reviewer + +Der v1.0.0-Sweep hat 3 Critical und 21 Major Findings hochgespült. Drei Klassen davon waren besonders lehrreich: + +- **`Equals`-Methoden die `GetHashCode()` vergleichen.** Klassisches Hash-Kollisions-Anti-Pattern. Klingt nach "ist doch egal, wenn Hashes gleich sind, sind die Objekte auch gleich", ist aber genau falsch. Hashes können kollidieren, Objekte sind dann nicht gleich. +- **`Dispose`-Methoden die nur einen Teil der Subscriptions wieder abmelden.** Leak bei jedem Plugin-Reload. Im Nutzer-Alltag merkst du das nicht sofort, im Long-Running-Test schon. +- **TOCTOU-Races.** Zwischen Bounds-Check und Read kann ein anderer Thread das Array unter dir austauschen (`GlobalParametersCache`, `AutoTranslate`). + +Davon hatte ich vorher bestenfalls die Theorie gelesen, nicht selbst diagnostiziert. CodeRabbit war für mich der Moment, wo "akademisches Wissen" zu "okay, das ist mein Code, das ist mein Bug" wurde. + +### Externe Tester sind ihr Gewicht in Gold wert + +Carlas Feedback zur Pop-Out-Discoverability hat den Header-Button in v0.6.1 ausgelöst. Dass Pop-Outs nur per Rechtsklick erreichbar waren, hatte ich als Maintainer nicht mehr gesehen, ich kannte den Pfad blind. Carls Wunsch nach Theme-Varianten mit Helligkeits-Abstufungen hat mein Verständnis von "ein Theme = eine Farbe" auf "Theme-Familien mit Stimmungs-Varianten" verschoben. Jingliu hat TempTell-Persistence gefordert, was das Tab-System architektonisch in Frage stellt. + +Solo hätte ich diese drei Dinge nicht erkannt. Punkt. + +### release.yml und die Markdown-Hölle + +Der `release.yml`-Workflow ist beim ersten v0.6.0-Tag-Push einfach nicht losgegangen. Ich habe Stunden in Permissions, Secret-Scopes und Tag-Trigger-Konfiguration gegraben, bevor ich verstand, was eigentlich los war: Der PowerShell-Heredoc-Footer im "Generate release body"-Step enthielt eine `---`-Markdown-Horizontal-Rule an Spalte 1, und genau das hat das YAML-Block-Scalar von `run: |` beendet. GitHub konnte die Workflow-Datei nicht parsen, also hat der Push-Tag-Trigger nie registriert. + +Fix: Footer in eine externe `.github/release-footer.md` extrahiert, Workflow liest sie via `Get-Content` ein. Lektion: Wenn ein Workflow nicht triggert, verifiziere als Erstes, dass GitHub die Datei überhaupt parsen kann. Das war einer der Bugs, bei denen ich nach dem Fix kurz gelacht habe und mich dann gefragt, wie viele andere YAML-Dateien ich noch habe, die so eine Falle drin haben könnten. + +--- + +## Was ich noch lerne + +### Performance-Profiling im Game-Context + +Der FPS-Drop-Bug aus Upstream Chat 2 ([#145](https://github.com/Infiziert90/ChatTwo/issues/145)) ist auch in Hellion Chat noch nicht reproduziert oder verifiziert. v1.0.0 hat mehrere Fixes auf den verdächtigen Pfaden (DbViewer O(N²) → O(N), AutoTranslate Lock-Serialisierung, EmoteCache HttpClient-Reuse), aber das systematische Vermessen unter Last fehlt mir. Ich muss noch lernen, wie man im Plugin-Kontext sauber misst, was wirklich das Frame-Budget frisst. + +### Native-Interop und Pointer-Math + +Auch nach dem WrapText-Span-Refactor in v0.5.4 ist mir Pointer-Math unsicher. ImGui zwingt einen an mehreren Stellen in `unsafe`-Code, und der Sicherheitsabstand zur "unbounded ArrayPool allocation"-Klasse von Bugs ist schmaler als mir lieb ist. Da will ich besser werden, bevor ich tieferes ImGui-Custom-Drawing anfasse. + +### Test-Disziplin für Plugin-Code + +Aktuell hat das Repo kein Test-Projekt. Das ist eine bewusste Entscheidung, keine vergessene. Plugin-Code mit FFXIV-Hooks und Dalamud-Lifecycle sauber zu testen ist nicht trivial, und ich hatte keinen Ansatz gefunden, der ohne riesiges Mocking-Gerüst sinnvoll wirkte. Privacy-Filter und Configuration-Migration wären gute Testkandidaten, weil sie isoliert sind. Steht auf der Liste, ist aber kein Quick-Win. + +### Linux-Eigenheiten unter Wine + +XDG-Compliance, libnotify-Integration, WireGuard-Network-Detection, alles in der [Roadmap](ROADMAP.md), und alles technisch noch nicht ganz klar. Wine und sandboxed Plugin-Code teilen nicht alle System-APIs, und ich weiß nicht, wo die Stolperfallen liegen, bevor ich sie gefunden habe. + +--- + +## Einsatz von AI-Tools + +Ich verwende Claude Code als Hilfsmittel, nicht als Ersatz für eigene Arbeit. + +**Wofür ich AI einsetze:** + +- Debugging von Problemen, bei denen ich nach längerer Eigenrecherche nicht weiterkomme +- Mustererkennen über große Codebasen hinweg (z. B. der ChatTwo→HellionChat-Sweep über 80 Dateien) +- Verständnisfragen zu C#- und Dalamud-Konzepten, die mir noch nicht geläufig sind +- Code-Review-Sparring, bevor ich CodeRabbit drauflasse + +**Was ich selbst mache:** + +- Architektur und Designentscheidungen +- Privacy-First-Defaults und das Threat-Model dahinter +- Tester-Kommunikation und Roadmap-Priorisierung +- Reviewen, Verifizieren, Pushen + +Die Klassifikation und konkrete Beispiele stehen in [`AI_DISCLOSURE.md`](AI_DISCLOSURE.md). Mir ist wichtig, dass Nutzer und potenzielle Beiträger verstehen, wie der Code zustande gekommen ist, gerade bei einem Plugin, das mit Nutzerdaten arbeitet. + +Ja, AI. Ja, alleine. Beides öfter erwähnt als nötig. Willkommen im Open-Source-Plugin-Klima. + +--- + +## Warum diese Transparenz + +Wer sich den Quellcode ansieht, soll wissen: + +- Ich bin kein professioneller C#- oder Plugin-Entwickler und lerne weiterhin dazu +- AI-Unterstützung ist ein Werkzeug, kein Ghostwriter +- Die Privacy-Position, die Designentscheidungen und die Roadmap sind meine +- Ich versuche, meinen Code so sauber und sicher zu halten, wie meine aktuellen Fähigkeiten es zulassen + +Hellion Chat ist auch ein Lernprojekt, und das soll man dem Repository ansehen dürfen. + +--- + +## Verlinkungen + +- [`AI_DISCLOSURE.md`](AI_DISCLOSURE.md) — KI-Pair-Disclosure mit Klassifikations-Schema +- [`CONTRIBUTORS.md`](CONTRIBUTORS.md) — wer hat dieses Plugin neben mir besser gemacht +- [`../NOTICE.md`](../NOTICE.md) — Anerkennung an Infi und Anna für das Chat-2-Fundament +- [`ROADMAP.md`](ROADMAP.md) — geplante Cycles und Themen diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 0000000..90756bb --- /dev/null +++ b/docs/ROADMAP.md @@ -0,0 +1,131 @@ +# Hellion Chat — Roadmap + +Geplante Arbeit nach dem v1.0.0 Standalone-Cut. Diese Liste ist absichtlich +grob: konkrete Specs, Größenschätzungen und Repro-Steps liegen im +internen Backlog. Tracking nach außen läuft über +[GitHub Issues](https://github.com/JonKazama-Hellion/HellionChat/issues) +mit dem `roadmap`-Label, sobald ein Item für einen Cycle eingeplant ist. + +Reihenfolge ist Priorität, nicht Garantie. Items können sich verschieben +oder ganz wegfallen wenn sie sich beim Brainstorm als nicht passend zur +Privacy-First-Schnittmenge des Plugins erweisen. + +--- + +## Nächster Cycle (v1.1.0) + +- **Ad-Block / Spam-Filter** — Hybrid-Konzept aus eigenem Light-Filter und + optionaler `NoSoliciting`-IPC-Integration. Adressiert Werbe-Spam in + öffentlichen Channels und Tells. Größter Block des Cycles. +- **Receive-Suppressed-Tells-Toggle** — Auto-Tell-Tabs greift auch wenn ein + Drittplugin (z.B. XIVMessenger) die /tell-Anzeige global suppressed. + Gleicher Hook-Layer wie Ad-Block, deshalb gebündelt. + +## Mittelfristig (v1.1.x – v1.2.0) + +- **Plugin-weite Theme-Varianten** — über die ChatColours-Presets aus v0.6.0 + hinaus. Mehrere komplette Window-Themes (Frame, Surface, Border, Text) + inkl. Farbfamilien mit Helligkeits-Abstufungen. Anknüpfung an + Hellion-Online-Media-Brand-Themes (Event Horizon, Night Blue, Indigo Violet + und weitere). +- **Database-Viewer Inline-Search** — Volltext-Suche im DB-Viewer via + SQLite FTS5. Aktuell gibt es nur Datums- und Channel-Filter. +- **TempTell Persistence** — Pin-Toggle auf TempTell-Tabs damit ausgewählte + Tells einen Relog überleben. Tester-Wunsch von Jingliu. +- **FontManager Async-Refactor** — `LoadGameSymFontAsync` aus dem + blockierenden Plugin-Constructor herausziehen. Cold-Start-Hitching beim + ersten Plugin-Start beheben (Severity niedrig, Plugin ist funktional). +- **Separate Opacity Active vs. Inactive** — zweiter Slider für inaktive + Fenster-Deckkraft. Upstream lehnt das ab; wir können hier anders + entscheiden. +- **Failed-Tell-Notification** — sichtbare Nachricht bei /tell-Fail + (offline, restricted instance, blacklisted, world-mismatch) statt + stillem Failure. +- **Per-Tab Sound-Notification** — Sound-Toggle und optional eigene .wav + pro Tab, mit Mute-In-Combat-Option. + +## Langfrist (v1.x+) + +### Storage-Backends (drei Stufen Bestätigung) + +- MySQL/MariaDB-Backend für Multi-Device-Setups +- PostgreSQL-Backend +- AES-256-Verschlüsselung für sensible Channels mit lokalem Key + +### Linux-spezifisch + +- WireGuard-Network-Detection als optionaler Filter-Trigger +- libnotify-Integration für native Linux-Toasts +- XDG-Compliance (komplex unter Wine) + +### UX und Tab-Management + +- **Regex Tab Routing** — Plugin-Output-Spam in eigene Tabs, Tells + bestimmter Personen automatisch sortieren. Klar abgegrenzt zum Ad-Block: + Routing sortiert in Views, Block versteckt global. +- **Auto-Detect Duties** — Tab-Switch beim Duty-Start via Condition-Flag. +- **UX Bundle** — Vertical-Tab-Bar als Layout-Option, Shift+Mousewheel zum + Tab-Header-Scrollen ohne Aktivierung, globaler Hotkey zum Schließen des + aktiven Tabs. +- **Configure Tab Title** — konfigurierbares Tab-Title-Format + (Name / Name + abgekürzter World / voller Name / Custom), pro Tab + überschreibbar. +- **Name Display Options** — analog zu FFXIV-Vanilla (voller Name, Vorname + abgekürzt, Initialen), per-Channel-Override möglich. +- **Item & Flag Linking** — Outgoing: Shift-Klick auf Item/Flag sendet ins + fokussierte Plugin-Input. Incoming: Item-Links und Map-Coords klickbar. +- **Color Currently Selected Input Channel** — Channel-Selector-Button im + Input-Bar mit Channel-Farbe einfärben. +- **Plugin-Disclosure Pre-Send Filter** — konfigurierbare Wort-/Regex-Liste + blockiert das Senden mit Pre-Send-Confirm. Schutz vor versehentlicher + Plugin-Nennung in öffentlichen Channels. +- **Chat Clear on Name Change** — bei Charakter-Namensänderung lokalen + Verlauf migrieren oder löschen, Default Wipe für maximale Privacy. +- **Hide Plugin Window on NG+ Screen** — Hide-Logik um zusätzliche + Addon-Namen erweitern. +- **Kick from Novice Network** — Mentor-Nische, Context-Menü-Eintrag mit + Confirmation. +- **Text-to-Speech für /tell** — eingehende Tells via TTS, optional pro + Sender, mit Channel-Filter und Mute-In-Combat. Geringe Priorität. + +### Distribution und Branding + +- Hand-gezeichnetes Hellion-Logo (aktuell Platzhalter aus dem + Hellion-Online-Media-Brand-Repo) +- GitHub Action für automatischen `repo.json`-Sync nach Tag-Push +- Submission ans Dalamud-Main-Plugin-Repo (zusätzlich zum Custom-Repo) + +--- + +## Bug-Verifizierungen + +Aus dem Upstream-Issue-Tracker übernommen, in Hellion Chat 1.0.0 noch +nicht reproduziert oder verifiziert. Werden bei Gelegenheit gegen den +aktuellen Stand getestet. + +- **Right-Click Whisper Error** in Field Ops / Special Instances (Eureka, + Bozja, Occult Crescent, DRS) — Upstream + [#168](https://github.com/Infiziert90/ChatTwo/issues/168). Reply-Helper + scheint `@World`-Suffix zu schlucken. +- **FPS Drops with Plugin active** — Upstream + [#145](https://github.com/Infiziert90/ChatTwo/issues/145). 10–20 % Drop + seit upstream v1.29.19.0. v1.0.0 hat mehrere Fixes auf den verdächtigen + Pfaden, Repro-Test gegen aktuellen Stand offen. +- **Add Blacklist from Plugin Window** — Upstream + [#140](https://github.com/Infiziert90/ChatTwo/issues/140). Right-Click + Add-to-Blacklist wirft "Cannot locate character with that name", via + Vanilla-Chat funktioniert es. +- **DB-Viewer Column Sort** — sortiert State-Column lexikografisch statt + numerisch (10 vor 2). XIVIM + [#82](https://github.com/NightmareXIV/XIVInstantMessenger/issues/82), + Repro in Hellion Chat offen. + +--- + +## Lizenz-Boundary + +Hellion Chat ist EUPL-1.2-lizenziert. Konzept-Imports aus AGPL-3.0-Plugins +(z.B. XIV Instant Messenger) sind ausschließlich architektonische +Inspiration, kein Code-Port. Imports aus dem GPL-3.0-kompatiblen +Upstream-Bestand laufen weiter über +[`UPSTREAM_SYNC.md`](UPSTREAM_SYNC.md). diff --git a/THIRD_PARTY_NOTICES.md b/docs/THIRD_PARTY_NOTICES.md similarity index 90% rename from THIRD_PARTY_NOTICES.md rename to docs/THIRD_PARTY_NOTICES.md index f7a8491..825be25 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/docs/THIRD_PARTY_NOTICES.md @@ -10,7 +10,7 @@ Last reviewed: 2026-05-03 (HellionChat v0.5.4). ## Direct NuGet dependencies -Pinned in `ChatTwo/ChatTwo.csproj`. Versions reflect the v0.5.4 build. +Pinned in `HellionChat/HellionChat.csproj`. Versions reflect the v1.0.0 build. | Package | Version | Licence | Network | Purpose | | --- | --- | --- | --- | --- | @@ -50,7 +50,7 @@ HellionChat is a fork of [Chat 2](https://github.com/Infiziert90/ChatTwo) by Infiziert90 (Infi) and Anna Clemens, also licensed under EUPL-1.2. The bulk of the code, including the message store architecture, the channel logic, the hook system and the ImGui chat window, originates -from upstream. See `NOTICE.md` and `UPSTREAM_SYNC.md` for the +from upstream. See `../NOTICE.md` and `UPSTREAM_SYNC.md` for the attribution and the cherry-pick policy. --- @@ -62,8 +62,8 @@ components opens network connections on their own. All outbound traffic is initiated explicitly by HellionChat's own source files and is documented in `PRIVACY.md` under "Outbound network calls": -- `ChatTwo/EmoteCache.cs` → BetterTTV API + CDN (opt-out via setting) -- `ChatTwo/FontManager.cs` → Square Enix Lodestone font CDN (one-time +- `HellionChat/EmoteCache.cs` → BetterTTV API + CDN (opt-out via setting) +- `HellionChat/FontManager.cs` → Square Enix Lodestone font CDN (one-time download) --- @@ -73,7 +73,7 @@ and is documented in `PRIVACY.md` under "Outbound network calls": To regenerate the dependency inventory after a version bump: ```bash -dotnet list ChatTwo.sln package --include-transitive +dotnet list HellionChat.sln package --include-transitive ``` The "direct NuGet dependencies" table above only lists direct @@ -85,7 +85,7 @@ To re-audit the network-call inventory: ```bash grep -rn -E "HttpClient|HttpRequest|new Uri\(|https?://" \ - --include="*.cs" ChatTwo/ + --include="*.cs" HellionChat/ ``` Any new hit that is not a click-through (`Util.OpenLink`) or a diff --git a/UPSTREAM_SYNC.md b/docs/UPSTREAM_SYNC.md similarity index 100% rename from UPSTREAM_SYNC.md rename to docs/UPSTREAM_SYNC.md -- 2.52.0 From 393ef175bf457fb73d81e69b17e9a7c3d495ac74 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 09:20:31 +0200 Subject: [PATCH 007/169] chore(actions): Bump actions/setup-dotnet from 4 to 5 (#6) Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 4 to 5. - [Release notes](https://github.com/actions/setup-dotnet/releases) - [Commits](https://github.com/actions/setup-dotnet/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-dotnet dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/codeql.yml | 2 +- .github/workflows/release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c579df5..b2e4663 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: uses: actions/checkout@v6 - name: Setup .NET 10 - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: 10.0.x diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 228d646..49da25b 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -42,7 +42,7 @@ jobs: uses: actions/checkout@v6 - name: Setup .NET 10 - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: 10.0.x diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e8dd56d..6283609 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,7 +47,7 @@ jobs: ref: ${{ github.event.inputs.tag || github.ref }} - name: Setup .NET 10 - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: 10.0.x -- 2.52.0 From 09634b416d7905cfee250521bc4588f6e0929e4a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 09:21:00 +0200 Subject: [PATCH 008/169] chore(actions): Bump github/codeql-action from 3 to 4 (#7) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v3...v4) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 49da25b..9707aaa 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -55,7 +55,7 @@ jobs: Expand-Archive -Force -Path dalamud.zip -DestinationPath $hooks - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: languages: csharp build-mode: manual @@ -68,7 +68,7 @@ jobs: run: dotnet build HellionChat/HellionChat.csproj --configuration Release --no-restore - name: Perform CodeQL analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 with: category: /language:csharp @@ -82,12 +82,12 @@ jobs: uses: actions/checkout@v6 - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: languages: actions build-mode: none - name: Perform CodeQL analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 with: category: /language:actions -- 2.52.0 From 9fc8749d150aae01a8df11ae1a795e6dfa080816 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Mon, 4 May 2026 09:32:36 +0200 Subject: [PATCH 009/169] fix(repo): update stale ChatTwo paths in repo configs - .gitattributes: linguist-generated path was still pointing at the pre-v1.0.0 ChatTwo/Resources/ tree, which silently let the renamed HellionChat/Resources/Language.*.resx files leak into Linguist's language statistics - bug_report.yml: drop the "or ChatTwo" filter hint; the plugin only emits HellionChat.* into /xllog since the v1.0.0 standalone cut --- .gitattributes | 2 +- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitattributes b/.gitattributes index 26464c0..7dc4840 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,2 @@ # Generated files -ChatTwo/Resources/Language.*.resx linguist-generated=true \ No newline at end of file +HellionChat/Resources/Language.*.resx linguist-generated=true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 6a86c29..59e4c1c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -59,7 +59,7 @@ body: id: log attributes: label: Relevant /xllog excerpt - description: Filter for "HellionChat" or "ChatTwo" if the log is huge + description: Filter for "HellionChat" if the log is huge render: text - type: checkboxes -- 2.52.0 From 176474ec2a99b5e9547b4307c62928572fe434e6 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Mon, 4 May 2026 09:39:15 +0200 Subject: [PATCH 010/169] chore(deps): bump Pidgin from 3.3.0 to 3.5.1 Catches up the only direct NuGet dependency that drifted behind on the v1.0.0 standalone cut. The bump includes: - 3.4.0: AnyCharExcept performance optimisation for single-char inputs - 3.5.0: incremental parsing API in Pidgin.Incremental, public Expected constructors, SequenceTokenParser performance improvement - 3.5.1: CIString Unicode handling fix (relevant for non-ASCII channel/tab names) No security advisory drove this; rolling forward to align v1.0.0 with the current upstream of every direct dependency. dotnet restore + Release build verified locally, packages.lock.json regenerated. --- HellionChat/HellionChat.csproj | 2 +- HellionChat/packages.lock.json | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/HellionChat/HellionChat.csproj b/HellionChat/HellionChat.csproj index bfae8dc..19a0d32 100644 --- a/HellionChat/HellionChat.csproj +++ b/HellionChat/HellionChat.csproj @@ -28,7 +28,7 @@ without a major bump on the managed wrapper. --> - + diff --git a/HellionChat/packages.lock.json b/HellionChat/packages.lock.json index 1149114..8f6f61c 100644 --- a/HellionChat/packages.lock.json +++ b/HellionChat/packages.lock.json @@ -44,9 +44,9 @@ }, "Pidgin": { "type": "Direct", - "requested": "[3.3.0, )", - "resolved": "3.3.0", - "contentHash": "2rvIoIogQG1+vqvXCuz1xiAVljaiacG/wCz/TNpN74TzWw+9iSCjhBLf7kVg24sBi6tArRdrcklHq49ovW2NLA==" + "requested": "[3.5.1, )", + "resolved": "3.5.1", + "contentHash": "zU7tkXlF3D6d2GLTjJDomAL3nnl4AwfZvSgSz8D4b+Ry21/clqedYlxBnEAkAU/bkGfEv6uRR7QCdWZUpKrB/g==" }, "SixLabors.ImageSharp": { "type": "Direct", -- 2.52.0 From 7012e8c0d887b28ce48dbc93332a29d787ddb491 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Mon, 4 May 2026 12:01:43 +0200 Subject: [PATCH 011/169] feat(window): recover off-screen position after display layout change Persisted ImGui window position can end up off-screen when the user disconnects a monitor or changes display resolution between sessions. The chat log window then renders outside the visible viewport with no drag handles available, and the only recovery path is editing the JSON config by hand. This commit adds two layers of safety: - Automatic one-shot bounds check on the first draw after plugin load. If less than 100x40 pixels of the saved window position overlap the primary viewport, the window snaps to a safe default offset (top-left + 50px). Logged at INF level so users can verify the recovery happened. - Manual "Reset Window Position" button in Settings -> Window -> Frame as a deliberate escape hatch when anything else slips past the automatic check (different DPI scaling, viewport edge cases). Pop-outs are intentionally not part of this recovery path: they are non-persistent (cleared on plugin reload) and therefore cannot survive a session boundary in an off-screen state. Tested on Linux/Wayland (KAZAMA, Plasma, 3-monitor setup): hard-cut test with both auxiliary monitors physically disconnected between sessions reproduces the off-screen window before the patch and recovers cleanly with this fix in place. --- .../Resources/HellionStrings.Designer.cs | 4 ++ HellionChat/Resources/HellionStrings.de.resx | 6 ++ HellionChat/Resources/HellionStrings.resx | 6 ++ HellionChat/Ui/ChatLogWindow.cs | 67 +++++++++++++++++++ HellionChat/Ui/SettingsTabs/Window.cs | 9 +++ 5 files changed, 92 insertions(+) diff --git a/HellionChat/Resources/HellionStrings.Designer.cs b/HellionChat/Resources/HellionStrings.Designer.cs index 5a4db83..fb80313 100644 --- a/HellionChat/Resources/HellionStrings.Designer.cs +++ b/HellionChat/Resources/HellionStrings.Designer.cs @@ -261,6 +261,10 @@ internal class HellionStrings internal static string Settings_Window_PopOutInputEnabled_Name => Get(nameof(Settings_Window_PopOutInputEnabled_Name)); internal static string Settings_Window_PopOutInputEnabled_Description => Get(nameof(Settings_Window_PopOutInputEnabled_Description)); + // Hellion Chat — Window position recovery (off-screen safety net) + internal static string Settings_Window_ResetPosition_Name => Get(nameof(Settings_Window_ResetPosition_Name)); + internal static string Settings_Window_ResetPosition_Description => Get(nameof(Settings_Window_ResetPosition_Description)); + // Hellion Chat — v0.6.0 one-time hint banner shown inside pop-outs internal static string Popout_v060_HintText => Get(nameof(Popout_v060_HintText)); internal static string Popout_v060_HintAck => Get(nameof(Popout_v060_HintAck)); diff --git a/HellionChat/Resources/HellionStrings.de.resx b/HellionChat/Resources/HellionStrings.de.resx index 177116d..9f88ea0 100644 --- a/HellionChat/Resources/HellionStrings.de.resx +++ b/HellionChat/Resources/HellionStrings.de.resx @@ -591,6 +591,12 @@ Master-Switch: erlaubt direktes Tippen und Absenden in jedem Pop-Out-Fenster (inkl. Auto-Tell-Tabs). Channel-Wechsel im Pop-Out wirkt global wie im Hauptfenster; Text-Buffer und History-Cursor sind pro Pop-Out unabhängig. + + Fenster-Position zurücksetzen + + + Holt das Chat-Fenster und alle aktiven Pop-Outs zurück in die linke obere Ecke des Hauptmonitors. Hilfreich wenn ein Fenster nach einem Display-Layout-Wechsel außerhalb des sichtbaren Bereichs gelandet ist (Monitor abgezogen, Auflösung geändert). Das Plugin macht außerdem einmal pro Session einen automatischen Bounds-Check, dieser Button ist der manuelle Notausgang falls trotzdem etwas unerreichbar bleibt. + Neu in v0.6.0: Du kannst jetzt direkt im Pop-Out tippen. Master-Switch in den Fenster-Settings aktivieren. diff --git a/HellionChat/Resources/HellionStrings.resx b/HellionChat/Resources/HellionStrings.resx index c6c9703..aefa6c6 100644 --- a/HellionChat/Resources/HellionStrings.resx +++ b/HellionChat/Resources/HellionStrings.resx @@ -591,6 +591,12 @@ Master switch: lets you type and send messages directly inside every pop-out window (including auto-tell tabs). Channel changes inside a pop-out apply globally just like in the main window; the text buffer and history cursor stay independent per pop-out. + + Reset Window Position + + + Snaps the chat window and every active pop-out back to the primary monitor's top-left corner. Useful when a window has drifted off-screen after a display layout change (monitor disconnected, resolution changed). The plugin also runs an automatic bounds check once per session — this button is the manual backup if anything still ends up unreachable. + New in v0.6.0: you can type directly inside pop-out windows. Toggle the master switch in the window settings to enable it. diff --git a/HellionChat/Ui/ChatLogWindow.cs b/HellionChat/Ui/ChatLogWindow.cs index fe7e201..64d2db3 100644 --- a/HellionChat/Ui/ChatLogWindow.cs +++ b/HellionChat/Ui/ChatLogWindow.cs @@ -66,6 +66,14 @@ public sealed class ChatLogWindow : Window public Vector2 LastWindowPos { get; private set; } = Vector2.Zero; public Vector2 LastWindowSize { get; private set; } = Vector2.Zero; + // Window position recovery: guards against off-screen positions after a + // display layout change (monitor disconnected, resolution changed). On + // the first draw after plugin load we run a one-shot bounds check to see + // whether the stored position still overlaps any visible viewport area. + // The manual reset button in the settings forces the position regardless. + private bool DidOnLoadBoundsCheck; + internal bool RequestPositionReset { get; set; } + public unsafe ImGuiViewport* LastViewport; private bool WasDocked; @@ -542,6 +550,22 @@ public sealed class ChatLogWindow : Window LastWindowSize = currentSize; LastWindowPos = ImGui.GetWindowPos(); + // Window position recovery. Manual reset takes precedence and snaps + // the window to the safe default unconditionally; the one-shot + // on-load check only fires when the persisted position has no + // overlap with any visible viewport area. + if (RequestPositionReset) + { + RequestPositionReset = false; + DidOnLoadBoundsCheck = true; + ApplySafeDefaultPosition("manual-reset"); + } + else if (!DidOnLoadBoundsCheck) + { + DidOnLoadBoundsCheck = true; + EnsureWindowOnScreen("on-load"); + } + if (resized) LastResize.Restart(); @@ -2035,4 +2059,47 @@ public sealed class ChatLogWindow : Window var hashCode = $"{Salt}{playerName}{worldId}".GetHashCode(); return $"Player {hashCode:X8}"; } + + // Snap threshold in pixels: at least this much of the window must overlap + // a visible viewport so the user can still grab the first tab header. + // Below the threshold the window is considered off-screen. + private const int OnScreenMinOverlapX = 100; + private const int OnScreenMinOverlapY = 40; + + // Default snap position relative to the primary viewport (top-left with a + // safety margin from the game title bar). + private static readonly Vector2 SafeDefaultOffset = new(50, 50); + + private void EnsureWindowOnScreen(string source) + { + if (LastWindowSize.X < 1 || LastWindowSize.Y < 1) + return; + + var viewport = ImGui.GetMainViewport(); + var visibleMin = viewport.WorkPos; + var visibleMax = viewport.WorkPos + viewport.WorkSize; + + var overlapMin = Vector2.Max(LastWindowPos, visibleMin); + var overlapMax = Vector2.Min(LastWindowPos + LastWindowSize, visibleMax); + var overlap = overlapMax - overlapMin; + + if (overlap.X >= OnScreenMinOverlapX && overlap.Y >= OnScreenMinOverlapY) + return; + + ApplySafeDefaultPosition(source); + } + + private void ApplySafeDefaultPosition(string source) + { + var viewport = ImGui.GetMainViewport(); + var safePos = viewport.WorkPos + SafeDefaultOffset; + Position = safePos; + Plugin.Log.Info( + $"[Window-Recovery] {source}: snapping main window from {LastWindowPos} (size {LastWindowSize}) to {safePos}."); + + // Pop-outs are intentionally non-persistent (cleared on plugin reload), + // so an off-screen pop-out can never survive a session boundary. The + // main window above is the only persistence target that needs an + // explicit recovery path. + } } diff --git a/HellionChat/Ui/SettingsTabs/Window.cs b/HellionChat/Ui/SettingsTabs/Window.cs index 8d42587..5a7dfdc 100644 --- a/HellionChat/Ui/SettingsTabs/Window.cs +++ b/HellionChat/Ui/SettingsTabs/Window.cs @@ -142,6 +142,15 @@ internal sealed class Window : ISettingsTab ImGui.Checkbox(Language.Options_SidebarTabView_Name, ref Mutable.SidebarTabView); ImGuiUtil.HelpMarker(string.Format(Language.Options_SidebarTabView_Description, Plugin.PluginName)); + + ImGui.Spacing(); + + // Manual escape hatch for off-screen windows. The plugin already + // runs an automatic bounds check once per session, but a button + // is the user-friendly fallback after a display layout change. + if (ImGui.Button(HellionStrings.Settings_Window_ResetPosition_Name)) + Plugin.ChatLogWindow.RequestPositionReset = true; + ImGuiUtil.HelpMarker(HellionStrings.Settings_Window_ResetPosition_Description); } } -- 2.52.0 From fcb72e2b78f9de9e788c3432cb7e4bcea507c11a Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Mon, 4 May 2026 12:15:26 +0200 Subject: [PATCH 012/169] =?UTF-8?q?chore(release):=20prepare=20v1.0.1=20?= =?UTF-8?q?=E2=80=94=20window=20position=20recovery?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps version to 1.0.1.0 and aligns the user-facing changelog across HellionChat.csproj, HellionChat.yaml, repo.json and docs/CHANGELOG.md. Headline fix: off-screen window recovery (one-shot bounds check on plugin load + manual reset button under Settings -> Window -> Frame). Bundled housekeeping since v1.0.0: docs restructured into docs/, stale ChatTwo/* paths cleaned up across configs, Pidgin 3.3.0 -> 3.5.1, actions/setup-dotnet 4 -> 5, github/codeql-action 3 -> 4. DLL build verified locally; release.yml workflow generates the release body from HellionChat.yaml on tag push. --- HellionChat/HellionChat.csproj | 2 +- HellionChat/HellionChat.yaml | 26 ++++++++++++++++++++++++++ docs/CHANGELOG.md | 16 ++++++++++++++++ repo.json | 12 ++++++------ 4 files changed, 49 insertions(+), 7 deletions(-) diff --git a/HellionChat/HellionChat.csproj b/HellionChat/HellionChat.csproj index 19a0d32..8251761 100644 --- a/HellionChat/HellionChat.csproj +++ b/HellionChat/HellionChat.csproj @@ -4,7 +4,7 @@ 0.1.0 is our bootstrap release; the underlying Chat 2 base is called out in the yaml changelog so users can see what it derives from. --> - 1.0.0 + 1.0.1 enable diff --git a/HellionChat/HellionChat.yaml b/HellionChat/HellionChat.yaml index e5f76d3..bbfffc0 100755 --- a/HellionChat/HellionChat.yaml +++ b/HellionChat/HellionChat.yaml @@ -49,6 +49,32 @@ tags: - Replacement - Privacy changelog: |- + **Hellion Chat 1.0.1 — Window Position Recovery** + + - Automatic bounds check on the first draw after plugin load. + When the persisted window position has no overlap with the + primary viewport, the window snaps to a safe top-left default. + Helpful after a monitor disconnect, resolution change or + multi-monitor layout switch between sessions. + - New "Reset Window Position" button in Settings → Window → Frame + as a manual escape hatch for edge cases the automatic check + doesn't catch. + + Tested on Linux/Wayland with a hard-cut three-monitor reduction; + window recovers cleanly without manual JSON editing. + + Housekeeping carried over since v1.0.0: + + - Documentation restructured into docs/ folder. New CHANGELOG, + CONTRIBUTORS, LEARNING-JOURNEY and ROADMAP added + - Stale ChatTwo/* paths in repo configs updated to HellionChat/* + - Pidgin parser library bumped from 3.3.0 to 3.5.1 (CIString + Unicode fix relevant for non-ASCII channel/tab names) + - GitHub Actions: actions/setup-dotnet bumped 4 → 5, + github/codeql-action bumped 3 → 4 + + Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2). + **Hellion Chat 1.0.0 — Standalone Major Release** First fully standalone release. Internal cleanup plus a sweep of diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 9950f89..0f05605 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -12,6 +12,22 @@ und verlinkt für Details auf die Release-Pages. --- +## [1.0.1] — 2026-05-04 — Window Position Recovery + +Fixes an off-screen-window scenario the user could end up in after a +monitor disconnect or display layout change between sessions. An +automatic one-shot bounds check on the first draw after plugin load +snaps the window back into the visible viewport, and a new +"Reset Window Position" button in Settings → Window → Frame serves as +the manual escape hatch for edge cases. + +Bundled housekeeping since v1.0.0: documentation restructured into +`docs/`, stale ChatTwo/* paths in repo configs cleaned up, Pidgin +parser library bumped from 3.3.0 to 3.5.1, GitHub Actions bumps for +`actions/setup-dotnet` (4 → 5) and `github/codeql-action` (3 → 4). + +[Release-Notes 1.0.1](https://github.com/JonKazama-Hellion/HellionChat/releases/tag/v1.0.1) + ## [1.0.0] — 2026-05-03 — Standalone Major Release Erste vollständig eigenständige Version. Code-Namespace, IPC-Kanäle und diff --git a/repo.json b/repo.json index d7e8c28..0f1b4de 100644 --- a/repo.json +++ b/repo.json @@ -3,7 +3,7 @@ "Author": "JonKazama-Hellion", "Name": "Hellion Chat", "InternalName": "HellionChat", - "AssemblyVersion": "1.0.0.0", + "AssemblyVersion": "1.0.1.0", "Description": "Hellion Chat is a privacy-focused chat replacement for FINAL FANTASY XIV based on the Chat 2 codebase (EUPL-1.2). One feature is intentionally removed (the optional webinterface) and a stack of privacy controls is added on top. Tabs, channel filters, RGB colours, emotes, screenshot mode, IPC integration and the chat replacement window itself work the same. The webinterface is intentionally not part of Hellion Chat because it serves a different use case from the smaller default footprint this plugin is built around.\n\nOn top of that, Hellion Chat adds privacy and data-handling controls designed to align with the modern data protection rules that apply across the EU, the United States and Japan. By default only your own conversations are stored; messages from strangers, NPCs and system spam stay out of the database. Retention windows are configurable per channel, history can be wiped retroactively, and stored data can be exported on demand.\n\nKey privacy and data-handling features:\n\n- Channel whitelist with a Privacy-First default\n- Per-channel retention with a daily background sweep\n- Retroactive cleanup with a Ctrl+Shift confirm\n- Export to Markdown, JSON or CSV\n- First-run wizard with three preset profiles (Privacy-First, Casual, Full History)\n- Bilingual UI (English and German) with live language switching\n- Independent plugin state — own config file and database directory, so Hellion Chat does not share state with upstream Chat 2\n\nBased on Chat 2 by Infi and Anna, licensed under EUPL-1.2.\n\nModding & support: join the Hellion Forge Discord at https://discord.gg/X9V7Kcv5gR — community for Hellion Chat and other Hellion Online Media plugins/tools.", "ApplicableVersion": "any", "RepoUrl": "https://github.com/JonKazama-Hellion/HellionChat", @@ -20,12 +20,12 @@ "CanUnloadAsync": false, "LoadPriority": 0, "Punchline": "Chat replacement with privacy controls aligned to EU, US and JP rules — based on Chat 2 (EUPL-1.2)", - "Changelog": "**Hellion Chat 1.0.0 — Standalone Major Release**\n\nFirst fully standalone release. Internal cleanup plus a sweep of\npre-existing correctness, security, threading and resource-leak\nfixes carried over from the upstream codebase. No user action\nrequired — auto-update applies cleanly, configuration and database\npaths unchanged.\n\nStandalone identity:\n\n- Code namespace consolidated from ChatTwo.* to HellionChat.* across\n all source files\n- IPC channels migrated from ChatTwo.* to HellionChat.* (6 channels:\n Register, Available, Unregister, Invoke, GetChatInputState,\n ChatInputStateChanged) — third-party plugins that bound to the old\n channels need to be updated; none known at release time\n- ImGui popup ID renamed to hellionchat-context-popup\n- Repository folder restructured (ChatTwo/ → HellionChat/), all CI\n and build paths updated accordingly\n- Public-facing descriptions reworded from upstream-fork framing to\n standalone framing (Chat 2 attribution preserved per EUPL-1.2)\n- Colour preset 'ChatTwo Default' is now 'Klassik (Chat 2 Default)'\n\nSafety:\n\n- Plugin now refuses to load when upstream Chat 2 is also active —\n bilingual conflict message in EN/DE, throw before any subsystem\n initialization, prevents the runtime crash that previously occurred\n when both plugins replaced the same chat window in parallel\n- SQLite native binary bumped to 3.50.3 (CVE-2025-6965 memory\n corruption from aggregate-term overflow, CVE-2025-7709)\n- NuGet restore now honors packages.lock.json so transitive\n dependencies don't drift between machines or CI runs\n\nDefault tab layout sharpened (one-time tab reset on first start):\n\nThe first-run tab layout is reorganized into five thematic tabs\nbased on external tester feedback. General contains only Say,\nYell and Shout (immediate-surroundings public chat). System\nabsorbs the gameplay-event streams (NpcDialogue, Loot, Crafting,\nGathering, PF recruitment pings) and announcement noise\n(BattleSystem, FreeCompanyAnnouncement, PvpTeamAnnouncement)\nthat previously lived in General. FreeCompany, Group and\nLinkshell each own their channel set. The static Tell tab is\ngone — Auto-Tell-Tabs spawns per-conversation tabs on demand.\nThe Beginner / Novice-Network preset is no longer added by\ndefault but is still available via Settings, Tabs.\n\nThis is a one-time tab-layout reset for users on config version\n12 or older. Privacy, Retention, Theme and every other setting\nis preserved. Your previous tab configuration is written to\npluginConfigs/HellionChat.json.pre-v13-backup so you can restore\nit manually if you prefer the old layout.\n\nCrash-class fixes (formerly latent in upstream):\n\n- MathUtil.HasOverlap now uses a correct AABB test; identical or\n edge-touching rectangles are no longer reported as non-overlapping\n- ChatCode.Equals compares fields directly instead of GetHashCode;\n removes the hash-collision anti-pattern\n- IpcManager.Dispose uses UnregisterAction to match the matching\n RegisterAction call; previous mismatch leaked the action\n subscription on every plugin reload\n- ExtraChat.Dispose now unsubscribes all three IPC subscriptions\n (was only the first); leaks closed\n- TellTarget.FromTarget guards against a zero IPlayerCharacter.Address\n before dereferencing the unsafe Character* cast\n- GameFunctions ResolveTextCommandPlaceholderDetour null-checks the\n Hook reference instead of using the null-forgiving operator\n- Popout.cs and SettingsTabs/Tabs.cs bounds-check list indexing so\n a tab drop or empty-worlds list no longer crashes the UI\n- Debugger.cs now declares IDisposable so the existing Dispose runs\n\nCorrectness fixes:\n\n- GlobalParametersCache.GetValue captures Cache into a local before\n the bounds check, so a concurrent Refresh can't slip a different\n array between check and read\n- IconUtil binary search bounds initialized to entries.Length-1 and\n reset on redirect-restart; entries.Length==0 short-circuits\n- Sheets.WorldsOnDatacenter now compares DataCenter.RowId (was\n Region.RowId) so it actually returns same-DC worlds\n- Message.cs back-reference loop iterates the processed Sender/Content\n properties so chunks added by CheckMessageContent get Message set\n- Language.zh-Hans Webinterface_Start_Success corrected to\n \"网页界面已启动\" (was \"网页界面已停止\")\n\nThreading and async:\n\n- AutoTranslate Entries/ValidEntries are now serialized behind a\n single lock; the preload worker thread and main thread no longer\n race on the underlying dictionary/hash set\n- Privacy retention and cleanup workers bound their framework-refresh\n waits to 5 seconds with a logged timeout; a hung framework tick can\n no longer deadlock the background worker\n\nResource handling:\n\n- EmoteCache reuses the static HttpClient instead of allocating a new\n one per call (closed socket leak)\n- FontManager wraps HttpClient/HttpResponseMessage in using-blocks\n and adds EnsureSuccessStatusCode; failed downloads no longer\n silently produce a zero-byte font file\n- SearchSelector mixes the row index into the ImGui ID stack so\n selectables don't collapse to a single ambiguous ID\n- SettingsTabs/Chat blocked-emote add-button now opens its selector\n popup on left-click\n\nPerformance:\n\n- DbViewer text export caches filteredHistory.Count once instead of\n re-enumerating the IEnumerable on every batch (O(N) instead of\n O(N²) on large histories)\n\nLicense attribution (NOTICE.md, COPYRIGHT, THIRD_PARTY_NOTICES.md\nand the Credits section in README) is unchanged.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 0.6.1 — Pop-Out Discoverability & /tell Auto-Pop-Out**\n\n- Pop-out button now visible in the chat header (no more hunting through the right-click menu)\n- One-time hint banner explains pop-out tabs and the right-click shortcut\n- New setting: open new /tell tabs directly as pop-out windows (Settings → Chat → Auto-Tell-Tabs)\n- Pop-out input is now enabled by default — closing a pop-out still returns the tab to the sidebar\n- Bugfix: dropping or logging out with an LRU/popped auto-tell tab now also closes its pop-out window (no more ghost windows)\n- Bugfix: dead zone below the chat input bar when the v0.6.0 pop-out hint banner was visible (also fixed retroactively for the v0.6.0 banner inside pop-outs)\n\nModding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 0.6.0 — UX Polish: Pop-Out Input + Colour Presets**\n\nTwo opt-in UX features land in the same release. Existing users see\nno change unless they enable the new toggles.\n\nPop-out input bar:\n\n- New global master switch in Settings → Window → Frame: \"Enable input\n in pop-outs\". Default OFF so existing behaviour is preserved\n- When enabled, every pop-out window grows a compact input bar at the\n bottom (channel-coloured icon button left, text input right). The\n auto-translate picker is intentionally not part of the compact bar\n in v0.6.0 — typical pop-out workflows (FC greeter, club hostess)\n rarely need it there\n- Each pop-out keeps an independent text buffer and history cursor;\n channel changes still apply globally because that is how the FFXIV\n channel API works\n- Up/Down navigates a shared input history singleton across the main\n window and every open pop-out\n- First pop-out opening after the upgrade shows a one-time hint\n banner pointing users to the new toggle\n\nChat colour presets:\n\n- Seven built-in presets above the per-channel colour list in\n Settings → Appearance → Colours: ChatTwo Default, High-Contrast,\n Pastell, Dark-Mode-Tuned, Hellion (brand-coloured, blue/orange\n Arctic Cyan + Ember Glow palette from the Hellion Online Media\n branding spec), plus two bonus mood presets — Night Blue (royal\n blue, classic-cool) and Indigo Violet (royal violet, glitter-mystic)\n- Apply is immediate and overwrites the channels covered by the\n preset; battle-channel colours are left alone so combat tuning\n stays intact\n\nConfiguration migrates from v10 to v11 with a diagnostic log entry;\nno data is reset. Bilingual (English/German) for both new sections.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 0.5.4 — WrapText hardening**\n\nReplaces the unsafe pointer-arithmetic in ImGuiUtil.WrapText with\nSpan- and index-based control flow. Closes the persistent CodeQL\nCritical alert \"unvalidated local pointer arithmetic\" that kept\nre-firing on every shape of the previous fix.\n\nHardening:\n\n- WrapText now allocates a buffer sized by Encoding.UTF8.GetMaxByteCount\n via ArrayPool, validates the actual encoded length against that\n ceiling, and threads the rest of the algorithm through int offsets\n instead of raw byte pointers\n- Pointer arithmetic only happens inside two small private helpers\n (CalcWordWrap and DrawText) that take the pinned base pointer plus\n int offsets sourced from the plugin's own logic, not from any\n virtual-method return\n- Added a 16 KiB upper bound on the buffer rent to prevent a\n pathological input from triggering an unbounded ArrayPool allocation\n\nNo user-visible behaviour change. Word-wrap output is byte-identical\nto v0.5.3.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 0.5.3 — Pointer arithmetic hardening**\n\nClosed CodeQL Critical alert in ImGuiUtil.WrapText by validating the\nencoded byte buffer length via GetByteCount before pointer\narithmetic. Single-fix patch on top of v0.5.2.\n\n---\n\nEarlier history: https://github.com/JonKazama-Hellion/HellionChat/releases", + "Changelog": "**Hellion Chat 1.0.1 — Window Position Recovery**\n\n- Automatic bounds check on the first draw after plugin load. When the persisted window position has no overlap with the primary viewport, the window snaps to a safe top-left default. Helpful after a monitor disconnect, resolution change or multi-monitor layout switch between sessions.\n- New \"Reset Window Position\" button in Settings → Window → Frame as a manual escape hatch for edge cases the automatic check doesn't catch.\n\nTested on Linux/Wayland with a hard-cut three-monitor reduction; window recovers cleanly without manual JSON editing.\n\nHousekeeping carried over since v1.0.0:\n\n- Documentation restructured into docs/ folder. New CHANGELOG, CONTRIBUTORS, LEARNING-JOURNEY and ROADMAP added\n- Stale ChatTwo/* paths in repo configs updated to HellionChat/*\n- Pidgin parser library bumped from 3.3.0 to 3.5.1 (CIString Unicode fix relevant for non-ASCII channel/tab names)\n- GitHub Actions: actions/setup-dotnet bumped 4 → 5, github/codeql-action bumped 3 → 4\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 1.0.0 — Standalone Major Release**\n\nFirst fully standalone release. Internal cleanup plus a sweep of\npre-existing correctness, security, threading and resource-leak\nfixes carried over from the upstream codebase. No user action\nrequired — auto-update applies cleanly, configuration and database\npaths unchanged.\n\nStandalone identity:\n\n- Code namespace consolidated from ChatTwo.* to HellionChat.* across\n all source files\n- IPC channels migrated from ChatTwo.* to HellionChat.* (6 channels:\n Register, Available, Unregister, Invoke, GetChatInputState,\n ChatInputStateChanged) — third-party plugins that bound to the old\n channels need to be updated; none known at release time\n- ImGui popup ID renamed to hellionchat-context-popup\n- Repository folder restructured (ChatTwo/ → HellionChat/), all CI\n and build paths updated accordingly\n- Public-facing descriptions reworded from upstream-fork framing to\n standalone framing (Chat 2 attribution preserved per EUPL-1.2)\n- Colour preset 'ChatTwo Default' is now 'Klassik (Chat 2 Default)'\n\nSafety:\n\n- Plugin now refuses to load when upstream Chat 2 is also active —\n bilingual conflict message in EN/DE, throw before any subsystem\n initialization, prevents the runtime crash that previously occurred\n when both plugins replaced the same chat window in parallel\n- SQLite native binary bumped to 3.50.3 (CVE-2025-6965 memory\n corruption from aggregate-term overflow, CVE-2025-7709)\n- NuGet restore now honors packages.lock.json so transitive\n dependencies don't drift between machines or CI runs\n\nDefault tab layout sharpened (one-time tab reset on first start):\n\nThe first-run tab layout is reorganized into five thematic tabs\nbased on external tester feedback. General contains only Say,\nYell and Shout (immediate-surroundings public chat). System\nabsorbs the gameplay-event streams (NpcDialogue, Loot, Crafting,\nGathering, PF recruitment pings) and announcement noise\n(BattleSystem, FreeCompanyAnnouncement, PvpTeamAnnouncement)\nthat previously lived in General. FreeCompany, Group and\nLinkshell each own their channel set. The static Tell tab is\ngone — Auto-Tell-Tabs spawns per-conversation tabs on demand.\nThe Beginner / Novice-Network preset is no longer added by\ndefault but is still available via Settings, Tabs.\n\nThis is a one-time tab-layout reset for users on config version\n12 or older. Privacy, Retention, Theme and every other setting\nis preserved. Your previous tab configuration is written to\npluginConfigs/HellionChat.json.pre-v13-backup so you can restore\nit manually if you prefer the old layout.\n\nCrash-class fixes (formerly latent in upstream):\n\n- MathUtil.HasOverlap now uses a correct AABB test; identical or\n edge-touching rectangles are no longer reported as non-overlapping\n- ChatCode.Equals compares fields directly instead of GetHashCode;\n removes the hash-collision anti-pattern\n- IpcManager.Dispose uses UnregisterAction to match the matching\n RegisterAction call; previous mismatch leaked the action\n subscription on every plugin reload\n- ExtraChat.Dispose now unsubscribes all three IPC subscriptions\n (was only the first); leaks closed\n- TellTarget.FromTarget guards against a zero IPlayerCharacter.Address\n before dereferencing the unsafe Character* cast\n- GameFunctions ResolveTextCommandPlaceholderDetour null-checks the\n Hook reference instead of using the null-forgiving operator\n- Popout.cs and SettingsTabs/Tabs.cs bounds-check list indexing so\n a tab drop or empty-worlds list no longer crashes the UI\n- Debugger.cs now declares IDisposable so the existing Dispose runs\n\nCorrectness fixes:\n\n- GlobalParametersCache.GetValue captures Cache into a local before\n the bounds check, so a concurrent Refresh can't slip a different\n array between check and read\n- IconUtil binary search bounds initialized to entries.Length-1 and\n reset on redirect-restart; entries.Length==0 short-circuits\n- Sheets.WorldsOnDatacenter now compares DataCenter.RowId (was\n Region.RowId) so it actually returns same-DC worlds\n- Message.cs back-reference loop iterates the processed Sender/Content\n properties so chunks added by CheckMessageContent get Message set\n- Language.zh-Hans Webinterface_Start_Success corrected to\n \"网页界面已启动\" (was \"网页界面已停止\")\n\nThreading and async:\n\n- AutoTranslate Entries/ValidEntries are now serialized behind a\n single lock; the preload worker thread and main thread no longer\n race on the underlying dictionary/hash set\n- Privacy retention and cleanup workers bound their framework-refresh\n waits to 5 seconds with a logged timeout; a hung framework tick can\n no longer deadlock the background worker\n\nResource handling:\n\n- EmoteCache reuses the static HttpClient instead of allocating a new\n one per call (closed socket leak)\n- FontManager wraps HttpClient/HttpResponseMessage in using-blocks\n and adds EnsureSuccessStatusCode; failed downloads no longer\n silently produce a zero-byte font file\n- SearchSelector mixes the row index into the ImGui ID stack so\n selectables don't collapse to a single ambiguous ID\n- SettingsTabs/Chat blocked-emote add-button now opens its selector\n popup on left-click\n\nPerformance:\n\n- DbViewer text export caches filteredHistory.Count once instead of\n re-enumerating the IEnumerable on every batch (O(N) instead of\n O(N²) on large histories)\n\nLicense attribution (NOTICE.md, COPYRIGHT, THIRD_PARTY_NOTICES.md\nand the Credits section in README) is unchanged.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 0.6.1 — Pop-Out Discoverability & /tell Auto-Pop-Out**\n\n- Pop-out button now visible in the chat header (no more hunting through the right-click menu)\n- One-time hint banner explains pop-out tabs and the right-click shortcut\n- New setting: open new /tell tabs directly as pop-out windows (Settings → Chat → Auto-Tell-Tabs)\n- Pop-out input is now enabled by default — closing a pop-out still returns the tab to the sidebar\n- Bugfix: dropping or logging out with an LRU/popped auto-tell tab now also closes its pop-out window (no more ghost windows)\n- Bugfix: dead zone below the chat input bar when the v0.6.0 pop-out hint banner was visible (also fixed retroactively for the v0.6.0 banner inside pop-outs)\n\nModding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 0.6.0 — UX Polish: Pop-Out Input + Colour Presets**\n\nTwo opt-in UX features land in the same release. Existing users see\nno change unless they enable the new toggles.\n\nPop-out input bar:\n\n- New global master switch in Settings → Window → Frame: \"Enable input\n in pop-outs\". Default OFF so existing behaviour is preserved\n- When enabled, every pop-out window grows a compact input bar at the\n bottom (channel-coloured icon button left, text input right). The\n auto-translate picker is intentionally not part of the compact bar\n in v0.6.0 — typical pop-out workflows (FC greeter, club hostess)\n rarely need it there\n- Each pop-out keeps an independent text buffer and history cursor;\n channel changes still apply globally because that is how the FFXIV\n channel API works\n- Up/Down navigates a shared input history singleton across the main\n window and every open pop-out\n- First pop-out opening after the upgrade shows a one-time hint\n banner pointing users to the new toggle\n\nChat colour presets:\n\n- Seven built-in presets above the per-channel colour list in\n Settings → Appearance → Colours: ChatTwo Default, High-Contrast,\n Pastell, Dark-Mode-Tuned, Hellion (brand-coloured, blue/orange\n Arctic Cyan + Ember Glow palette from the Hellion Online Media\n branding spec), plus two bonus mood presets — Night Blue (royal\n blue, classic-cool) and Indigo Violet (royal violet, glitter-mystic)\n- Apply is immediate and overwrites the channels covered by the\n preset; battle-channel colours are left alone so combat tuning\n stays intact\n\nConfiguration migrates from v10 to v11 with a diagnostic log entry;\nno data is reset. Bilingual (English/German) for both new sections.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 0.5.4 — WrapText hardening**\n\nReplaces the unsafe pointer-arithmetic in ImGuiUtil.WrapText with\nSpan- and index-based control flow. Closes the persistent CodeQL\nCritical alert \"unvalidated local pointer arithmetic\" that kept\nre-firing on every shape of the previous fix.\n\nHardening:\n\n- WrapText now allocates a buffer sized by Encoding.UTF8.GetMaxByteCount\n via ArrayPool, validates the actual encoded length against that\n ceiling, and threads the rest of the algorithm through int offsets\n instead of raw byte pointers\n- Pointer arithmetic only happens inside two small private helpers\n (CalcWordWrap and DrawText) that take the pinned base pointer plus\n int offsets sourced from the plugin's own logic, not from any\n virtual-method return\n- Added a 16 KiB upper bound on the buffer rent to prevent a\n pathological input from triggering an unbounded ArrayPool allocation\n\nNo user-visible behaviour change. Word-wrap output is byte-identical\nto v0.5.3.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 0.5.3 — Pointer arithmetic hardening**\n\nClosed CodeQL Critical alert in ImGuiUtil.WrapText by validating the\nencoded byte buffer length via GetByteCount before pointer\narithmetic. Single-fix patch on top of v0.5.2.\n\n---\n\nEarlier history: https://github.com/JonKazama-Hellion/HellionChat/releases", "AcceptsFeedback": true, - "DownloadLinkInstall": "https://github.com/JonKazama-Hellion/HellionChat/releases/download/v1.0.0/latest.zip", - "DownloadLinkUpdate": "https://github.com/JonKazama-Hellion/HellionChat/releases/download/v1.0.0/latest.zip", - "DownloadLinkTesting": "https://github.com/JonKazama-Hellion/HellionChat/releases/download/v1.0.0/latest.zip", - "TestingAssemblyVersion": "1.0.0.0", + "DownloadLinkInstall": "https://github.com/JonKazama-Hellion/HellionChat/releases/download/v1.0.1/latest.zip", + "DownloadLinkUpdate": "https://github.com/JonKazama-Hellion/HellionChat/releases/download/v1.0.1/latest.zip", + "DownloadLinkTesting": "https://github.com/JonKazama-Hellion/HellionChat/releases/download/v1.0.1/latest.zip", + "TestingAssemblyVersion": "1.0.1.0", "IconUrl": "https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/icon.png", "ImageUrls": [ "https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/chatWindow.png", -- 2.52.0 From 8e9332ac8c1e4743c28761bbe110ef37f1d55c3f Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Mon, 4 May 2026 15:57:52 +0200 Subject: [PATCH 013/169] =?UTF-8?q?chore(release):=20prepare=20v1.0.2=20?= =?UTF-8?q?=E2=80=94=20polish=20patch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four small backlog items bundled: - New: hide chat (and every other plugin window) while the New Game+ menu is open. Settings -> Window -> Frame, default off. Skips the whole WindowSystem.Draw() pass while QuestRedo is visible, mirroring the existing HideInLoadingScreens pattern. - New: tint the channel selector button in the active channel colour. Settings -> Appearance -> Colours, default on. Reuses the existing inputColour computation (incl. ExtraChat override) and adds an ImGuiCol.Button push around the selector. New ColourUtil helper AdjustBrightness derives hover/active variants. - Fix: PayloadHandler.InlineIcon hardcoded all hover icons to 32x32. Replaced with float-based aspect-ratio-preserving shrink, single scale-factor, zero-size guard, named MaxInlineIconSize constant. Affects six call sites (status, item, achievement and other inline hover paths). - Diagnostic: HideState transitions log on Verbose level for both ChatLogWindow and Popout. Manifest bumped to 1.0.2 across csproj, yaml, repo.json. CHANGELOG entry added, README version line updated. yaml + repo.json changelog trimmed to the slim 4-version window (1.0.2, 1.0.1, 1.0.0, 0.6.1). --- HellionChat/Configuration.cs | 4 + HellionChat/GameFunctions/GameFunctions.cs | 2 + HellionChat/HellionChat.csproj | 2 +- HellionChat/HellionChat.yaml | 88 +++++----------------- HellionChat/PayloadHandler.cs | 11 ++- HellionChat/Plugin.cs | 10 +++ HellionChat/Resources/Language.Designer.cs | 38 +++++++++- HellionChat/Resources/Language.de.resx | 12 +++ HellionChat/Resources/Language.resx | 12 +++ HellionChat/Ui/ChatLogWindow.cs | 76 +++++++++++++------ HellionChat/Ui/Popout.cs | 18 +++++ HellionChat/Ui/SettingsTabs/Appearance.cs | 4 + HellionChat/Ui/SettingsTabs/Window.cs | 3 + HellionChat/Util/ColourUtil.cs | 14 ++++ README.md | 2 +- docs/CHANGELOG.md | 27 +++++++ repo.json | 6 +- 17 files changed, 230 insertions(+), 99 deletions(-) diff --git a/HellionChat/Configuration.cs b/HellionChat/Configuration.cs index 7d33dda..f1f9c85 100755 --- a/HellionChat/Configuration.cs +++ b/HellionChat/Configuration.cs @@ -145,6 +145,7 @@ public class Configuration : IPluginConfiguration public bool HideWhenUiHidden = true; public bool HideInLoadingScreens; public bool HideInBattle; + public bool HideInNewGamePlusMenu; public bool HideWhenInactive; public int InactivityHideTimeout = 10; public bool InactivityHideActiveDuringBattle = true; @@ -221,6 +222,7 @@ public class Configuration : IPluginConfiguration public float TooltipOffset; public float WindowAlpha = 100f; public Dictionary ChatColours = new(); + public bool ColorSelectedInputChannelButton = true; public List Tabs = []; public bool OverrideStyle; @@ -241,6 +243,7 @@ public class Configuration : IPluginConfiguration HideWhenUiHidden = other.HideWhenUiHidden; HideInLoadingScreens = other.HideInLoadingScreens; HideInBattle = other.HideInBattle; + HideInNewGamePlusMenu = other.HideInNewGamePlusMenu; HideWhenInactive = other.HideWhenInactive; InactivityHideTimeout = other.InactivityHideTimeout; InactivityHideActiveDuringBattle = other.InactivityHideActiveDuringBattle; @@ -288,6 +291,7 @@ public class Configuration : IPluginConfiguration TooltipOffset = other.TooltipOffset; WindowAlpha = other.WindowAlpha; ChatColours = other.ChatColours.ToDictionary(entry => entry.Key, entry => entry.Value); + ColorSelectedInputChannelButton = other.ColorSelectedInputChannelButton; // Hellion Chat — Auto-Tell-Tabs are session-only and therefore // never present in a disk-loaded copy. Keep the live temp tabs of diff --git a/HellionChat/GameFunctions/GameFunctions.cs b/HellionChat/GameFunctions/GameFunctions.cs index 31b8970..c05b8f7 100755 --- a/HellionChat/GameFunctions/GameFunctions.cs +++ b/HellionChat/GameFunctions/GameFunctions.cs @@ -20,6 +20,8 @@ namespace HellionChat.GameFunctions; internal unsafe class GameFunctions : IDisposable { + internal const string NewGamePlusAddonName = "QuestRedo"; + #region Hooks [Signature("E8 ?? ?? ?? ?? 48 85 C0 0F 84 ?? ?? ?? ?? 48 8B D0 49 8D 4F", DetourName = nameof(ResolveTextCommandPlaceholderDetour))] private Hook? ResolveTextCommandPlaceholderHook = null!; diff --git a/HellionChat/HellionChat.csproj b/HellionChat/HellionChat.csproj index 8251761..dc7f418 100644 --- a/HellionChat/HellionChat.csproj +++ b/HellionChat/HellionChat.csproj @@ -4,7 +4,7 @@ 0.1.0 is our bootstrap release; the underlying Chat 2 base is called out in the yaml changelog so users can see what it derives from. --> - 1.0.1 + 1.0.2 enable diff --git a/HellionChat/HellionChat.yaml b/HellionChat/HellionChat.yaml index bbfffc0..a8b26a9 100755 --- a/HellionChat/HellionChat.yaml +++ b/HellionChat/HellionChat.yaml @@ -49,6 +49,24 @@ tags: - Replacement - Privacy changelog: |- + **Hellion Chat 1.0.2 — Polish patch** + + - New: optionally hide chat (and every other plugin window) while the + New Game+ menu is open. Toggle in Settings → Window → Frame, default + off. Closing the menu restores all windows. + - New: optionally tint the channel selector button next to the input + field with the currently active channel's colour. Toggle in + Settings → Appearance → Colours, default on. Matches the existing + input-text tint and respects ExtraChat overrides. + - Fix: status, item and other inline hover icons keep their original + aspect ratio. Debuff icons with non-square dimensions are no longer + visually squished into a 32×32 box. + - Diagnostic: hide-state transitions (battle, cutscene, user-hide, + cutscene override) are now logged on Verbose level for easier bug + reports — off by default, enable with `/xllog set HellionChat verbose`. + + Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2). + **Hellion Chat 1.0.1 — Window Position Recovery** - Automatic bounds check on the first draw after plugin load. @@ -214,76 +232,6 @@ changelog: |- Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2). - **Hellion Chat 0.6.0 — UX Polish: Pop-Out Input + Colour Presets** - - Two opt-in UX features land in the same release. Existing users see - no change unless they enable the new toggles. - - Pop-out input bar: - - - New global master switch in Settings → Window → Frame: "Enable input - in pop-outs". Default OFF so existing behaviour is preserved - - When enabled, every pop-out window grows a compact input bar at the - bottom (channel-coloured icon button left, text input right). The - auto-translate picker is intentionally not part of the compact bar - in v0.6.0 — typical pop-out workflows (FC greeter, club hostess) - rarely need it there - - Each pop-out keeps an independent text buffer and history cursor; - channel changes still apply globally because that is how the FFXIV - channel API works - - Up/Down navigates a shared input history singleton across the main - window and every open pop-out - - First pop-out opening after the upgrade shows a one-time hint - banner pointing users to the new toggle - - Chat colour presets: - - - Seven built-in presets above the per-channel colour list in - Settings → Appearance → Colours: ChatTwo Default, High-Contrast, - Pastell, Dark-Mode-Tuned, Hellion (brand-coloured, blue/orange - Arctic Cyan + Ember Glow palette from the Hellion Online Media - branding spec), plus two bonus mood presets — Night Blue (royal - blue, classic-cool) and Indigo Violet (royal violet, glitter-mystic) - - Apply is immediate and overwrites the channels covered by the - preset; battle-channel colours are left alone so combat tuning - stays intact - - Configuration migrates from v10 to v11 with a diagnostic log entry; - no data is reset. Bilingual (English/German) for both new sections. - - Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2). - - **Hellion Chat 0.5.4 — WrapText hardening** - - Replaces the unsafe pointer-arithmetic in ImGuiUtil.WrapText with - Span- and index-based control flow. Closes the persistent CodeQL - Critical alert "unvalidated local pointer arithmetic" that kept - re-firing on every shape of the previous fix. - - Hardening: - - - WrapText now allocates a buffer sized by Encoding.UTF8.GetMaxByteCount - via ArrayPool, validates the actual encoded length against that - ceiling, and threads the rest of the algorithm through int offsets - instead of raw byte pointers - - Pointer arithmetic only happens inside two small private helpers - (CalcWordWrap and DrawText) that take the pinned base pointer plus - int offsets sourced from the plugin's own logic, not from any - virtual-method return - - Added a 16 KiB upper bound on the buffer rent to prevent a - pathological input from triggering an unbounded ArrayPool allocation - - No user-visible behaviour change. Word-wrap output is byte-identical - to v0.5.3. - - Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2). - - **Hellion Chat 0.5.3 — Pointer arithmetic hardening** - - Closed CodeQL Critical alert in ImGuiUtil.WrapText by validating the - encoded byte buffer length via GetByteCount before pointer - arithmetic. Single-fix patch on top of v0.5.2. - --- Earlier history: https://github.com/JonKazama-Hellion/HellionChat/releases diff --git a/HellionChat/PayloadHandler.cs b/HellionChat/PayloadHandler.cs index 380c09a..83d430c 100755 --- a/HellionChat/PayloadHandler.cs +++ b/HellionChat/PayloadHandler.cs @@ -332,10 +332,19 @@ public sealed class PayloadHandler atkBase->SetPosition((short) x, (short) y); } + private const float MaxInlineIconSize = 32f; + private static void InlineIcon(IDalamudTextureWrap icon) { + if (icon.Size.X <= 0 || icon.Size.Y <= 0) + return; + + var width = (float) icon.Size.X; + var height = (float) icon.Size.Y; + var scale = Math.Min(1f, Math.Min(MaxInlineIconSize / width, MaxInlineIconSize / height)); + var size = ImGuiHelpers.ScaledVector2(width * scale, height * scale); + var cursor = ImGui.GetCursorPos(); - var size = ImGuiHelpers.ScaledVector2(32, 32); ImGui.Image(icon.Handle, size); ImGui.SameLine(); ImGui.SetCursorPos(cursor + new Vector2(size.X + 4, size.Y - ImGui.GetTextLineHeightWithSpacing())); diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index 4bc24d8..62052c9 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -571,6 +571,16 @@ public sealed class Plugin : IDalamudPlugin return; } + // v1.0.2 — global skip while the New Game+ menu (QuestRedo addon) is + // open. Hides every plugin window in one shot (chat log, pop-outs, + // settings, db viewer, etc.), matching the LoadingScreens pattern. + if (Config.HideInNewGamePlusMenu && GameFunctions.GameFunctions.IsAddonInteractable(GameFunctions.GameFunctions.NewGamePlusAddonName)) + { + ChatLogWindow.FinalizeFrame(); + TypingIpc.Update(); + return; + } + ChatLogWindow.HideStateCheck(); Interface.UiBuilder.DisableUserUiHide = !Config.HideWhenUiHidden; diff --git a/HellionChat/Resources/Language.Designer.cs b/HellionChat/Resources/Language.Designer.cs index e40d551..bfc59e3 100755 --- a/HellionChat/Resources/Language.Designer.cs +++ b/HellionChat/Resources/Language.Designer.cs @@ -2148,6 +2148,24 @@ namespace HellionChat.Resources { } } + /// + /// Looks up a localized string similar to The channel selector button next to the input field is tinted with the currently active channel's colour. Matches the tinting of the input text itself.. + /// + internal static string Options_ColorSelectedInputChannelButton_Description { + get { + return ResourceManager.GetString("Options_ColorSelectedInputChannelButton_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tint channel selector with channel colour. + /// + internal static string Options_ColorSelectedInputChannelButton_Name { + get { + return ResourceManager.GetString("Options_ColorSelectedInputChannelButton_Name", resourceCulture); + } + } + /// /// Looks up a localized string similar to Chat colours. /// @@ -2660,7 +2678,25 @@ namespace HellionChat.Resources { return ResourceManager.GetString("Options_HideInBattle_Name", resourceCulture); } } - + + /// + /// Looks up a localized string similar to Hide the chat while the New Game+ menu is open. Closing the menu shows the chat again.. + /// + internal static string Options_HideInNewGamePlusMenu_Description { + get { + return ResourceManager.GetString("Options_HideInNewGamePlusMenu_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Hide while New Game+ menu is open. + /// + internal static string Options_HideInNewGamePlusMenu_Name { + get { + return ResourceManager.GetString("Options_HideInNewGamePlusMenu_Name", resourceCulture); + } + } + /// /// Looks up a localized string similar to Hide {0} during loading screens.. /// diff --git a/HellionChat/Resources/Language.de.resx b/HellionChat/Resources/Language.de.resx index 29cae9a..a3fbe20 100644 --- a/HellionChat/Resources/Language.de.resx +++ b/HellionChat/Resources/Language.de.resx @@ -208,6 +208,12 @@ Vom Spiel importieren + + Channel-Auswahl-Knopf in Channel-Farbe + + + Der Channel-Auswahl-Knopf neben dem Eingabefeld bekommt die Farbe des aktuell aktiven Channels. Konsistent zur Färbung des Eingabetextes selbst. + Kanäle @@ -1190,6 +1196,12 @@ Sie wurden gewarnt. Blende den Chat während der Kämpfe aus. + + Während des New-Game+ Menüs ausblenden + + + Blendet den Chat aus, solange das New-Game+ Menü geöffnet ist. Schließen des Menüs blendet den Chat wieder ein. + Emote-Statistik diff --git a/HellionChat/Resources/Language.resx b/HellionChat/Resources/Language.resx index c3a3f19..46512ce 100644 --- a/HellionChat/Resources/Language.resx +++ b/HellionChat/Resources/Language.resx @@ -208,6 +208,12 @@ Import from game + + Tint channel selector with channel colour + + + The channel selector button next to the input field is tinted with the currently active channel's colour. Matches the tinting of the input text itself. + Tabs @@ -1189,6 +1195,12 @@ Hide the chat during battles. + + Hide while New Game+ menu is open + + + Hide the chat while the New Game+ menu is open. Closing the menu shows the chat again. + Emote Stats diff --git a/HellionChat/Ui/ChatLogWindow.cs b/HellionChat/Ui/ChatLogWindow.cs index 64d2db3..ebeb8dc 100644 --- a/HellionChat/Ui/ChatLogWindow.cs +++ b/HellionChat/Ui/ChatLogWindow.cs @@ -278,9 +278,11 @@ public sealed class ChatLogWindow : Window { case "hide": CurrentHideState = HideState.User; + Plugin.Log.Verbose("HideState: → User (chat hide command)"); break; case "show": CurrentHideState = HideState.None; + Plugin.Log.Verbose("HideState: → None (chat show command)"); break; case "toggle": CurrentHideState = CurrentHideState switch @@ -290,6 +292,7 @@ public sealed class ChatLogWindow : Window HideState.None => HideState.User, _ => CurrentHideState, }; + Plugin.Log.Verbose($"HideState: → {CurrentHideState} (chat toggle command)"); break; } } @@ -406,30 +409,48 @@ public sealed class ChatLogWindow : Window { // if the chat has no hide state set, and the player has entered battle, we hide chat if they have configured it if (Plugin.Config.HideInBattle && CurrentHideState == HideState.None && Plugin.InBattle) + { CurrentHideState = HideState.Battle; + Plugin.Log.Verbose("HideState: None → Battle"); + } // If the chat is hidden because of battle, we reset it here if (CurrentHideState is HideState.Battle && !Plugin.InBattle) + { CurrentHideState = HideState.None; + Plugin.Log.Verbose("HideState: Battle → None"); + } // if the chat has no hide state and in a cutscene, set the hide state to cutscene if (Plugin.Config.HideDuringCutscenes && CurrentHideState == HideState.None && (Plugin.CutsceneActive || Plugin.GposeActive)) { if (Plugin.Functions.Chat.CheckHideFlags()) + { CurrentHideState = HideState.Cutscene; + Plugin.Log.Verbose("HideState: None → Cutscene"); + } } // if the chat is hidden because of a cutscene and no longer in a cutscene, set the hide state to none if (CurrentHideState is HideState.Cutscene or HideState.CutsceneOverride && !Plugin.CutsceneActive && !Plugin.GposeActive) + { + Plugin.Log.Verbose($"HideState: {CurrentHideState} → None (cutscene/gpose ended)"); CurrentHideState = HideState.None; + } // if the chat is hidden because of a cutscene and the chat has been activated, show chat if (CurrentHideState == HideState.Cutscene && Activate) + { CurrentHideState = HideState.CutsceneOverride; + Plugin.Log.Verbose("HideState: Cutscene → CutsceneOverride (user activate)"); + } // if the user hid the chat and is now activating chat, reset the hide state if (CurrentHideState == HideState.User && Activate) + { CurrentHideState = HideState.None; + Plugin.Log.Verbose("HideState: User → None (activate)"); + } if (CurrentHideState is HideState.Cutscene or HideState.User or HideState.Battle || (Plugin.Config.HideWhenNotLoggedIn && !Plugin.ClientState.IsLoggedIn)) { @@ -600,9 +621,40 @@ public sealed class ChatLogWindow : Window DrawChannelName(activeTab); } + // v1.0.2 — compute inputColour up front so the channel selector button + // can also tint with it (existing input-text push remains below). + var inputType = activeTab.CurrentChannel.UseTempChannel ? activeTab.CurrentChannel.TempChannel.ToChatType() : activeTab.CurrentChannel.Channel.ToChatType(); + var isCommand = Chat.Trim().StartsWith('/'); + if (isCommand) + { + var command = Chat.Split(' ')[0]; + if (TextCommandChannels.TryGetValue(command, out var channel)) + inputType = channel; + + if (!IsValidCommand(command)) + inputType = ChatType.Error; + } + + var inputColour = Plugin.Config.ChatColours.TryGetValue(inputType, out var inputCol) ? inputCol : inputType.DefaultColor(); + + if (!isCommand && Plugin.ExtraChat.ChannelOverride is var (_, overrideColour)) + inputColour = overrideColour; + + if (isCommand && Plugin.ExtraChat.ChannelCommandColours.TryGetValue(Chat.Split(' ')[0], out var ecColour)) + inputColour = ecColour; + var beforeIcon = ImGui.GetCursorPos(); - if (ImGuiUtil.IconButton(FontAwesomeIcon.Comment) && activeTab.Channel is null) - ImGui.OpenPopup(ChatChannelPicker); + + var tintSelector = Plugin.Config.ColorSelectedInputChannelButton && inputColour.HasValue; + var selectorAbgr = tintSelector ? ColourUtil.RgbaToAbgr(inputColour!.Value) : 0u; + + using (ImRaii.PushColor(ImGuiCol.Button, selectorAbgr, tintSelector)) + using (ImRaii.PushColor(ImGuiCol.ButtonHovered, ColourUtil.AdjustBrightness(selectorAbgr, 1.15f), tintSelector)) + using (ImRaii.PushColor(ImGuiCol.ButtonActive, ColourUtil.AdjustBrightness(selectorAbgr, 0.85f), tintSelector)) + { + if (ImGuiUtil.IconButton(FontAwesomeIcon.Comment) && activeTab.Channel is null) + ImGui.OpenPopup(ChatChannelPicker); + } if (activeTab.Channel is not null && ImGui.IsItemHovered()) ImGuiUtil.Tooltip(Language.ChatLog_SwitcherDisabled); @@ -626,27 +678,7 @@ public sealed class ChatLogWindow : Window var buttonsRight = (showNovice ? 1 : 0) + (Plugin.Config.ShowHideButton ? 1 : 0); var inputWidth = ImGui.GetContentRegionAvail().X - buttonWidth * (1 + buttonsRight); - var inputType = activeTab.CurrentChannel.UseTempChannel ? activeTab.CurrentChannel.TempChannel.ToChatType() : activeTab.CurrentChannel.Channel.ToChatType(); - var isCommand = Chat.Trim().StartsWith('/'); - if (isCommand) - { - var command = Chat.Split(' ')[0]; - if (TextCommandChannels.TryGetValue(command, out var channel)) - inputType = channel; - - if (!IsValidCommand(command)) - inputType = ChatType.Error; - } - var normalColor = ImGui.GetColorU32(ImGuiCol.Text); - var inputColour = Plugin.Config.ChatColours.TryGetValue(inputType, out var inputCol) ? inputCol : inputType.DefaultColor(); - - if (!isCommand && Plugin.ExtraChat.ChannelOverride is var (_, overrideColour)) - inputColour = overrideColour; - - if (isCommand && Plugin.ExtraChat.ChannelCommandColours.TryGetValue(Chat.Split(' ')[0], out var ecColour)) - inputColour = ecColour; - var push = inputColour != null; using (ImRaii.PushColor(ImGuiCol.Text, push ? ColourUtil.RgbaToAbgr(inputColour!.Value) : 0, push)) { diff --git a/HellionChat/Ui/Popout.cs b/HellionChat/Ui/Popout.cs index a2f34f8..01f3c5d 100644 --- a/HellionChat/Ui/Popout.cs +++ b/HellionChat/Ui/Popout.cs @@ -229,30 +229,48 @@ internal class Popout : Window { // if the chat has no hide state set, and the player has entered battle, we hide chat if they have configured it if (Tab.HideInBattle && CurrentHideState == HideState.None && Plugin.InBattle) + { CurrentHideState = HideState.Battle; + Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: None → Battle"); + } // If the chat is hidden because of battle, we reset it here if (CurrentHideState is HideState.Battle && !Plugin.InBattle) + { CurrentHideState = HideState.None; + Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: Battle → None"); + } // if the chat has no hide state and in a cutscene, set the hide state to cutscene if (Tab.HideDuringCutscenes && CurrentHideState == HideState.None && (Plugin.CutsceneActive || Plugin.GposeActive)) { if (ChatLogWindow.Plugin.Functions.Chat.CheckHideFlags()) + { CurrentHideState = HideState.Cutscene; + Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: None → Cutscene"); + } } // if the chat is hidden because of a cutscene and no longer in a cutscene, set the hide state to none if (CurrentHideState is HideState.Cutscene or HideState.CutsceneOverride && !Plugin.CutsceneActive && !Plugin.GposeActive) + { + Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: {CurrentHideState} → None (cutscene/gpose ended)"); CurrentHideState = HideState.None; + } // if the chat is hidden because of a cutscene and the chat has been activated, show chat if (CurrentHideState == HideState.Cutscene && ChatLogWindow.Activate) + { CurrentHideState = HideState.CutsceneOverride; + Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: Cutscene → CutsceneOverride (user activate)"); + } // if the user hid the chat and is now activating chat, reset the hide state if (CurrentHideState == HideState.User && ChatLogWindow.Activate) + { CurrentHideState = HideState.None; + Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: User → None (activate)"); + } return CurrentHideState is HideState.Cutscene or HideState.User or HideState.Battle || (Tab.HideWhenNotLoggedIn && !Plugin.ClientState.IsLoggedIn); } diff --git a/HellionChat/Ui/SettingsTabs/Appearance.cs b/HellionChat/Ui/SettingsTabs/Appearance.cs index 63eabaf..6517f74 100644 --- a/HellionChat/Ui/SettingsTabs/Appearance.cs +++ b/HellionChat/Ui/SettingsTabs/Appearance.cs @@ -236,6 +236,10 @@ internal sealed class Appearance : ISettingsTab ImGui.Separator(); ImGui.Spacing(); + ImGui.Checkbox(Language.Options_ColorSelectedInputChannelButton_Name, ref Mutable.ColorSelectedInputChannelButton); + ImGuiUtil.HelpMarker(Language.Options_ColorSelectedInputChannelButton_Description); + ImGui.Spacing(); + foreach (var (_, types) in ChatTypeExt.SortOrder) { foreach (var type in types) diff --git a/HellionChat/Ui/SettingsTabs/Window.cs b/HellionChat/Ui/SettingsTabs/Window.cs index 5a7dfdc..6372581 100644 --- a/HellionChat/Ui/SettingsTabs/Window.cs +++ b/HellionChat/Ui/SettingsTabs/Window.cs @@ -56,6 +56,9 @@ internal sealed class Window : ISettingsTab ImGui.Checkbox(Language.Options_HideInBattle_Name, ref Mutable.HideInBattle); ImGuiUtil.HelpMarker(Language.Options_HideInBattle_Description); + + ImGui.Checkbox(Language.Options_HideInNewGamePlusMenu_Name, ref Mutable.HideInNewGamePlusMenu); + ImGuiUtil.HelpMarker(Language.Options_HideInNewGamePlusMenu_Description); } } diff --git a/HellionChat/Util/ColourUtil.cs b/HellionChat/Util/ColourUtil.cs index d74e2f4..63214f0 100755 --- a/HellionChat/Util/ColourUtil.cs +++ b/HellionChat/Util/ColourUtil.cs @@ -48,4 +48,18 @@ internal static class ColourUtil { internal static uint ComponentsToRgba(byte red, byte green, byte blue, byte alpha = 0xFF) => alpha | (uint) (red << 24) | (uint) (green << 16) | (uint) (blue << 8); + + internal static uint AdjustBrightness(uint abgr, float factor) + { + var a = (byte) ((abgr & 0xFF000000) >> 24); + var b = (byte) ((abgr & 0x00FF0000) >> 16); + var g = (byte) ((abgr & 0x0000FF00) >> 8); + var r = (byte) (abgr & 0x000000FF); + + var nr = (byte) Math.Clamp(r * factor, 0f, 255f); + var ng = (byte) Math.Clamp(g * factor, 0f, 255f); + var nb = (byte) Math.Clamp(b * factor, 0f, 255f); + + return ((uint) a << 24) | ((uint) nb << 16) | ((uint) ng << 8) | nr; + } } diff --git a/README.md b/README.md index 8a6cb2b..95e121b 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![.NET](https://img.shields.io/badge/.NET-10.0-512BD4)](https://dotnet.microsoft.com/) [![FFXIV](https://img.shields.io/badge/FFXIV-Dawntrail-c3a37f)](https://www.finalfantasyxiv.com/) -**Version 1.0.0** — DSGVO-bewusster Chat-Ersatz für FINAL FANTASY XIV / Dalamud, basierend auf [Chat 2](https://github.com/Infiziert90/ChatTwo) (EUPL-1.2). +**Version 1.0.2** — DSGVO-bewusster Chat-Ersatz für FINAL FANTASY XIV / Dalamud, basierend auf [Chat 2](https://github.com/Infiziert90/ChatTwo) (EUPL-1.2). Hellion Chat ergänzt das ursprüngliche Chat-2-Fundament um Datenschutz- und Daten-Handling-Kontrollen, die mit den Datenschutz-Regeln in der EU, den USA und Japan im Einklang sind. Alle aus Chat 2 übernommenen Funktionen, Befehle und Tastenkürzel funktionieren unverändert. Eigenständiger Plugin-Slot, eigene Konfiguration, eigene Datenbank. diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 0f05605..15b37b7 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -12,6 +12,33 @@ und verlinkt für Details auf die Release-Pages. --- +## [1.0.2] — 2026-05-04 — Polish patch + +Vier kleine Polish-Items aus dem Backlog gebündelt: + +- **Hide bei New Game+ Menü**: Optionaler globaler Toggle der Hellion + Chat (und alle weiteren Plugin-Fenster wie Settings, DB-Viewer, + Pop-Outs) ausblendet, solange das NG+-Menü offen ist. Settings → + Fenster → Rahmen, Default aus. Skipt analog zum bestehenden + LoadingScreens-Pattern den gesamten `WindowSystem.Draw()`-Pfad. +- **Channel-Selector-Färbung**: Optionales Tinting des + Channel-Auswahl-Knopfs (Comment-Icon) neben dem Eingabefeld in der + aktuellen Channel-Farbe. Settings → Aussehen → Chat-Farben, Default + an. Konsistent zur bestehenden Eingabetext-Färbung, ExtraChat-Override + wird übernommen. +- **(De)Buff-Icon Aspect-Ratio-Fix**: `PayloadHandler.InlineIcon` quetschte + alle Hover-Icons auf 32×32. Status-Icons mit nicht-quadratischen + Dimensionen (Debuffs mit Pfeil-Indikator) sind jetzt aspekt-erhaltend + geshrinkt. Eigenständige Float-Math-Implementierung mit Zero-Size-Guard + statt Cherry-Pick aus dem offenen ChatTwo PR #157 (der hatte eine + int-Division-Falle). +- **HideState-Logging-Sweep**: Alle HideState-Transitions + (Battle/Cutscene/User/Override plus die Pop-Out-Spiegelung) loggen sich + auf Verbose-Level. Aus by default, Aktivierung via + `/xllog set HellionChat verbose` für Bug-Report-Diagnose. + +[Release-Notes 1.0.2](https://github.com/JonKazama-Hellion/HellionChat/releases/tag/v1.0.2) + ## [1.0.1] — 2026-05-04 — Window Position Recovery Fixes an off-screen-window scenario the user could end up in after a diff --git a/repo.json b/repo.json index 0f1b4de..7f5ed56 100644 --- a/repo.json +++ b/repo.json @@ -3,7 +3,7 @@ "Author": "JonKazama-Hellion", "Name": "Hellion Chat", "InternalName": "HellionChat", - "AssemblyVersion": "1.0.1.0", + "AssemblyVersion": "1.0.2.0", "Description": "Hellion Chat is a privacy-focused chat replacement for FINAL FANTASY XIV based on the Chat 2 codebase (EUPL-1.2). One feature is intentionally removed (the optional webinterface) and a stack of privacy controls is added on top. Tabs, channel filters, RGB colours, emotes, screenshot mode, IPC integration and the chat replacement window itself work the same. The webinterface is intentionally not part of Hellion Chat because it serves a different use case from the smaller default footprint this plugin is built around.\n\nOn top of that, Hellion Chat adds privacy and data-handling controls designed to align with the modern data protection rules that apply across the EU, the United States and Japan. By default only your own conversations are stored; messages from strangers, NPCs and system spam stay out of the database. Retention windows are configurable per channel, history can be wiped retroactively, and stored data can be exported on demand.\n\nKey privacy and data-handling features:\n\n- Channel whitelist with a Privacy-First default\n- Per-channel retention with a daily background sweep\n- Retroactive cleanup with a Ctrl+Shift confirm\n- Export to Markdown, JSON or CSV\n- First-run wizard with three preset profiles (Privacy-First, Casual, Full History)\n- Bilingual UI (English and German) with live language switching\n- Independent plugin state — own config file and database directory, so Hellion Chat does not share state with upstream Chat 2\n\nBased on Chat 2 by Infi and Anna, licensed under EUPL-1.2.\n\nModding & support: join the Hellion Forge Discord at https://discord.gg/X9V7Kcv5gR — community for Hellion Chat and other Hellion Online Media plugins/tools.", "ApplicableVersion": "any", "RepoUrl": "https://github.com/JonKazama-Hellion/HellionChat", @@ -20,12 +20,12 @@ "CanUnloadAsync": false, "LoadPriority": 0, "Punchline": "Chat replacement with privacy controls aligned to EU, US and JP rules — based on Chat 2 (EUPL-1.2)", - "Changelog": "**Hellion Chat 1.0.1 — Window Position Recovery**\n\n- Automatic bounds check on the first draw after plugin load. When the persisted window position has no overlap with the primary viewport, the window snaps to a safe top-left default. Helpful after a monitor disconnect, resolution change or multi-monitor layout switch between sessions.\n- New \"Reset Window Position\" button in Settings → Window → Frame as a manual escape hatch for edge cases the automatic check doesn't catch.\n\nTested on Linux/Wayland with a hard-cut three-monitor reduction; window recovers cleanly without manual JSON editing.\n\nHousekeeping carried over since v1.0.0:\n\n- Documentation restructured into docs/ folder. New CHANGELOG, CONTRIBUTORS, LEARNING-JOURNEY and ROADMAP added\n- Stale ChatTwo/* paths in repo configs updated to HellionChat/*\n- Pidgin parser library bumped from 3.3.0 to 3.5.1 (CIString Unicode fix relevant for non-ASCII channel/tab names)\n- GitHub Actions: actions/setup-dotnet bumped 4 → 5, github/codeql-action bumped 3 → 4\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 1.0.0 — Standalone Major Release**\n\nFirst fully standalone release. Internal cleanup plus a sweep of\npre-existing correctness, security, threading and resource-leak\nfixes carried over from the upstream codebase. No user action\nrequired — auto-update applies cleanly, configuration and database\npaths unchanged.\n\nStandalone identity:\n\n- Code namespace consolidated from ChatTwo.* to HellionChat.* across\n all source files\n- IPC channels migrated from ChatTwo.* to HellionChat.* (6 channels:\n Register, Available, Unregister, Invoke, GetChatInputState,\n ChatInputStateChanged) — third-party plugins that bound to the old\n channels need to be updated; none known at release time\n- ImGui popup ID renamed to hellionchat-context-popup\n- Repository folder restructured (ChatTwo/ → HellionChat/), all CI\n and build paths updated accordingly\n- Public-facing descriptions reworded from upstream-fork framing to\n standalone framing (Chat 2 attribution preserved per EUPL-1.2)\n- Colour preset 'ChatTwo Default' is now 'Klassik (Chat 2 Default)'\n\nSafety:\n\n- Plugin now refuses to load when upstream Chat 2 is also active —\n bilingual conflict message in EN/DE, throw before any subsystem\n initialization, prevents the runtime crash that previously occurred\n when both plugins replaced the same chat window in parallel\n- SQLite native binary bumped to 3.50.3 (CVE-2025-6965 memory\n corruption from aggregate-term overflow, CVE-2025-7709)\n- NuGet restore now honors packages.lock.json so transitive\n dependencies don't drift between machines or CI runs\n\nDefault tab layout sharpened (one-time tab reset on first start):\n\nThe first-run tab layout is reorganized into five thematic tabs\nbased on external tester feedback. General contains only Say,\nYell and Shout (immediate-surroundings public chat). System\nabsorbs the gameplay-event streams (NpcDialogue, Loot, Crafting,\nGathering, PF recruitment pings) and announcement noise\n(BattleSystem, FreeCompanyAnnouncement, PvpTeamAnnouncement)\nthat previously lived in General. FreeCompany, Group and\nLinkshell each own their channel set. The static Tell tab is\ngone — Auto-Tell-Tabs spawns per-conversation tabs on demand.\nThe Beginner / Novice-Network preset is no longer added by\ndefault but is still available via Settings, Tabs.\n\nThis is a one-time tab-layout reset for users on config version\n12 or older. Privacy, Retention, Theme and every other setting\nis preserved. Your previous tab configuration is written to\npluginConfigs/HellionChat.json.pre-v13-backup so you can restore\nit manually if you prefer the old layout.\n\nCrash-class fixes (formerly latent in upstream):\n\n- MathUtil.HasOverlap now uses a correct AABB test; identical or\n edge-touching rectangles are no longer reported as non-overlapping\n- ChatCode.Equals compares fields directly instead of GetHashCode;\n removes the hash-collision anti-pattern\n- IpcManager.Dispose uses UnregisterAction to match the matching\n RegisterAction call; previous mismatch leaked the action\n subscription on every plugin reload\n- ExtraChat.Dispose now unsubscribes all three IPC subscriptions\n (was only the first); leaks closed\n- TellTarget.FromTarget guards against a zero IPlayerCharacter.Address\n before dereferencing the unsafe Character* cast\n- GameFunctions ResolveTextCommandPlaceholderDetour null-checks the\n Hook reference instead of using the null-forgiving operator\n- Popout.cs and SettingsTabs/Tabs.cs bounds-check list indexing so\n a tab drop or empty-worlds list no longer crashes the UI\n- Debugger.cs now declares IDisposable so the existing Dispose runs\n\nCorrectness fixes:\n\n- GlobalParametersCache.GetValue captures Cache into a local before\n the bounds check, so a concurrent Refresh can't slip a different\n array between check and read\n- IconUtil binary search bounds initialized to entries.Length-1 and\n reset on redirect-restart; entries.Length==0 short-circuits\n- Sheets.WorldsOnDatacenter now compares DataCenter.RowId (was\n Region.RowId) so it actually returns same-DC worlds\n- Message.cs back-reference loop iterates the processed Sender/Content\n properties so chunks added by CheckMessageContent get Message set\n- Language.zh-Hans Webinterface_Start_Success corrected to\n \"网页界面已启动\" (was \"网页界面已停止\")\n\nThreading and async:\n\n- AutoTranslate Entries/ValidEntries are now serialized behind a\n single lock; the preload worker thread and main thread no longer\n race on the underlying dictionary/hash set\n- Privacy retention and cleanup workers bound their framework-refresh\n waits to 5 seconds with a logged timeout; a hung framework tick can\n no longer deadlock the background worker\n\nResource handling:\n\n- EmoteCache reuses the static HttpClient instead of allocating a new\n one per call (closed socket leak)\n- FontManager wraps HttpClient/HttpResponseMessage in using-blocks\n and adds EnsureSuccessStatusCode; failed downloads no longer\n silently produce a zero-byte font file\n- SearchSelector mixes the row index into the ImGui ID stack so\n selectables don't collapse to a single ambiguous ID\n- SettingsTabs/Chat blocked-emote add-button now opens its selector\n popup on left-click\n\nPerformance:\n\n- DbViewer text export caches filteredHistory.Count once instead of\n re-enumerating the IEnumerable on every batch (O(N) instead of\n O(N²) on large histories)\n\nLicense attribution (NOTICE.md, COPYRIGHT, THIRD_PARTY_NOTICES.md\nand the Credits section in README) is unchanged.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 0.6.1 — Pop-Out Discoverability & /tell Auto-Pop-Out**\n\n- Pop-out button now visible in the chat header (no more hunting through the right-click menu)\n- One-time hint banner explains pop-out tabs and the right-click shortcut\n- New setting: open new /tell tabs directly as pop-out windows (Settings → Chat → Auto-Tell-Tabs)\n- Pop-out input is now enabled by default — closing a pop-out still returns the tab to the sidebar\n- Bugfix: dropping or logging out with an LRU/popped auto-tell tab now also closes its pop-out window (no more ghost windows)\n- Bugfix: dead zone below the chat input bar when the v0.6.0 pop-out hint banner was visible (also fixed retroactively for the v0.6.0 banner inside pop-outs)\n\nModding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 0.6.0 — UX Polish: Pop-Out Input + Colour Presets**\n\nTwo opt-in UX features land in the same release. Existing users see\nno change unless they enable the new toggles.\n\nPop-out input bar:\n\n- New global master switch in Settings → Window → Frame: \"Enable input\n in pop-outs\". Default OFF so existing behaviour is preserved\n- When enabled, every pop-out window grows a compact input bar at the\n bottom (channel-coloured icon button left, text input right). The\n auto-translate picker is intentionally not part of the compact bar\n in v0.6.0 — typical pop-out workflows (FC greeter, club hostess)\n rarely need it there\n- Each pop-out keeps an independent text buffer and history cursor;\n channel changes still apply globally because that is how the FFXIV\n channel API works\n- Up/Down navigates a shared input history singleton across the main\n window and every open pop-out\n- First pop-out opening after the upgrade shows a one-time hint\n banner pointing users to the new toggle\n\nChat colour presets:\n\n- Seven built-in presets above the per-channel colour list in\n Settings → Appearance → Colours: ChatTwo Default, High-Contrast,\n Pastell, Dark-Mode-Tuned, Hellion (brand-coloured, blue/orange\n Arctic Cyan + Ember Glow palette from the Hellion Online Media\n branding spec), plus two bonus mood presets — Night Blue (royal\n blue, classic-cool) and Indigo Violet (royal violet, glitter-mystic)\n- Apply is immediate and overwrites the channels covered by the\n preset; battle-channel colours are left alone so combat tuning\n stays intact\n\nConfiguration migrates from v10 to v11 with a diagnostic log entry;\nno data is reset. Bilingual (English/German) for both new sections.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 0.5.4 — WrapText hardening**\n\nReplaces the unsafe pointer-arithmetic in ImGuiUtil.WrapText with\nSpan- and index-based control flow. Closes the persistent CodeQL\nCritical alert \"unvalidated local pointer arithmetic\" that kept\nre-firing on every shape of the previous fix.\n\nHardening:\n\n- WrapText now allocates a buffer sized by Encoding.UTF8.GetMaxByteCount\n via ArrayPool, validates the actual encoded length against that\n ceiling, and threads the rest of the algorithm through int offsets\n instead of raw byte pointers\n- Pointer arithmetic only happens inside two small private helpers\n (CalcWordWrap and DrawText) that take the pinned base pointer plus\n int offsets sourced from the plugin's own logic, not from any\n virtual-method return\n- Added a 16 KiB upper bound on the buffer rent to prevent a\n pathological input from triggering an unbounded ArrayPool allocation\n\nNo user-visible behaviour change. Word-wrap output is byte-identical\nto v0.5.3.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 0.5.3 — Pointer arithmetic hardening**\n\nClosed CodeQL Critical alert in ImGuiUtil.WrapText by validating the\nencoded byte buffer length via GetByteCount before pointer\narithmetic. Single-fix patch on top of v0.5.2.\n\n---\n\nEarlier history: https://github.com/JonKazama-Hellion/HellionChat/releases", + "Changelog": "**Hellion Chat 1.0.2 — Polish patch**\n\n- New: optionally hide chat (and every other plugin window) while the New Game+ menu is open. Toggle in Settings → Window → Frame, default off. Closing the menu restores all windows.\n- New: optionally tint the channel selector button next to the input field with the currently active channel's colour. Toggle in Settings → Appearance → Colours, default on. Matches the existing input-text tint and respects ExtraChat overrides.\n- Fix: status, item and other inline hover icons keep their original aspect ratio. Debuff icons with non-square dimensions are no longer visually squished into a 32×32 box.\n- Diagnostic: hide-state transitions (battle, cutscene, user-hide, cutscene override) are now logged on Verbose level for easier bug reports — off by default, enable with `/xllog set HellionChat verbose`.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 1.0.1 — Window Position Recovery**\n\n- Automatic bounds check on the first draw after plugin load. When the persisted window position has no overlap with the primary viewport, the window snaps to a safe top-left default. Helpful after a monitor disconnect, resolution change or multi-monitor layout switch between sessions.\n- New \"Reset Window Position\" button in Settings → Window → Frame as a manual escape hatch for edge cases the automatic check doesn't catch.\n\nTested on Linux/Wayland with a hard-cut three-monitor reduction; window recovers cleanly without manual JSON editing.\n\nHousekeeping carried over since v1.0.0:\n\n- Documentation restructured into docs/ folder. New CHANGELOG, CONTRIBUTORS, LEARNING-JOURNEY and ROADMAP added\n- Stale ChatTwo/* paths in repo configs updated to HellionChat/*\n- Pidgin parser library bumped from 3.3.0 to 3.5.1 (CIString Unicode fix relevant for non-ASCII channel/tab names)\n- GitHub Actions: actions/setup-dotnet bumped 4 → 5, github/codeql-action bumped 3 → 4\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 1.0.0 — Standalone Major Release**\n\nFirst fully standalone release. Internal cleanup plus a sweep of\npre-existing correctness, security, threading and resource-leak\nfixes carried over from the upstream codebase. No user action\nrequired — auto-update applies cleanly, configuration and database\npaths unchanged.\n\nStandalone identity:\n\n- Code namespace consolidated from ChatTwo.* to HellionChat.* across\n all source files\n- IPC channels migrated from ChatTwo.* to HellionChat.* (6 channels:\n Register, Available, Unregister, Invoke, GetChatInputState,\n ChatInputStateChanged) — third-party plugins that bound to the old\n channels need to be updated; none known at release time\n- ImGui popup ID renamed to hellionchat-context-popup\n- Repository folder restructured (ChatTwo/ → HellionChat/), all CI\n and build paths updated accordingly\n- Public-facing descriptions reworded from upstream-fork framing to\n standalone framing (Chat 2 attribution preserved per EUPL-1.2)\n- Colour preset 'ChatTwo Default' is now 'Klassik (Chat 2 Default)'\n\nSafety:\n\n- Plugin now refuses to load when upstream Chat 2 is also active —\n bilingual conflict message in EN/DE, throw before any subsystem\n initialization, prevents the runtime crash that previously occurred\n when both plugins replaced the same chat window in parallel\n- SQLite native binary bumped to 3.50.3 (CVE-2025-6965 memory\n corruption from aggregate-term overflow, CVE-2025-7709)\n- NuGet restore now honors packages.lock.json so transitive\n dependencies don't drift between machines or CI runs\n\nDefault tab layout sharpened (one-time tab reset on first start):\n\nThe first-run tab layout is reorganized into five thematic tabs\nbased on external tester feedback. General contains only Say,\nYell and Shout (immediate-surroundings public chat). System\nabsorbs the gameplay-event streams (NpcDialogue, Loot, Crafting,\nGathering, PF recruitment pings) and announcement noise\n(BattleSystem, FreeCompanyAnnouncement, PvpTeamAnnouncement)\nthat previously lived in General. FreeCompany, Group and\nLinkshell each own their channel set. The static Tell tab is\ngone — Auto-Tell-Tabs spawns per-conversation tabs on demand.\nThe Beginner / Novice-Network preset is no longer added by\ndefault but is still available via Settings, Tabs.\n\nThis is a one-time tab-layout reset for users on config version\n12 or older. Privacy, Retention, Theme and every other setting\nis preserved. Your previous tab configuration is written to\npluginConfigs/HellionChat.json.pre-v13-backup so you can restore\nit manually if you prefer the old layout.\n\nCrash-class fixes (formerly latent in upstream):\n\n- MathUtil.HasOverlap now uses a correct AABB test; identical or\n edge-touching rectangles are no longer reported as non-overlapping\n- ChatCode.Equals compares fields directly instead of GetHashCode;\n removes the hash-collision anti-pattern\n- IpcManager.Dispose uses UnregisterAction to match the matching\n RegisterAction call; previous mismatch leaked the action\n subscription on every plugin reload\n- ExtraChat.Dispose now unsubscribes all three IPC subscriptions\n (was only the first); leaks closed\n- TellTarget.FromTarget guards against a zero IPlayerCharacter.Address\n before dereferencing the unsafe Character* cast\n- GameFunctions ResolveTextCommandPlaceholderDetour null-checks the\n Hook reference instead of using the null-forgiving operator\n- Popout.cs and SettingsTabs/Tabs.cs bounds-check list indexing so\n a tab drop or empty-worlds list no longer crashes the UI\n- Debugger.cs now declares IDisposable so the existing Dispose runs\n\nCorrectness fixes:\n\n- GlobalParametersCache.GetValue captures Cache into a local before\n the bounds check, so a concurrent Refresh can't slip a different\n array between check and read\n- IconUtil binary search bounds initialized to entries.Length-1 and\n reset on redirect-restart; entries.Length==0 short-circuits\n- Sheets.WorldsOnDatacenter now compares DataCenter.RowId (was\n Region.RowId) so it actually returns same-DC worlds\n- Message.cs back-reference loop iterates the processed Sender/Content\n properties so chunks added by CheckMessageContent get Message set\n- Language.zh-Hans Webinterface_Start_Success corrected to\n \"网页界面已启动\" (was \"网页界面已停止\")\n\nThreading and async:\n\n- AutoTranslate Entries/ValidEntries are now serialized behind a\n single lock; the preload worker thread and main thread no longer\n race on the underlying dictionary/hash set\n- Privacy retention and cleanup workers bound their framework-refresh\n waits to 5 seconds with a logged timeout; a hung framework tick can\n no longer deadlock the background worker\n\nResource handling:\n\n- EmoteCache reuses the static HttpClient instead of allocating a new\n one per call (closed socket leak)\n- FontManager wraps HttpClient/HttpResponseMessage in using-blocks\n and adds EnsureSuccessStatusCode; failed downloads no longer\n silently produce a zero-byte font file\n- SearchSelector mixes the row index into the ImGui ID stack so\n selectables don't collapse to a single ambiguous ID\n- SettingsTabs/Chat blocked-emote add-button now opens its selector\n popup on left-click\n\nPerformance:\n\n- DbViewer text export caches filteredHistory.Count once instead of\n re-enumerating the IEnumerable on every batch (O(N) instead of\n O(N²) on large histories)\n\nLicense attribution (NOTICE.md, COPYRIGHT, THIRD_PARTY_NOTICES.md\nand the Credits section in README) is unchanged.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 0.6.1 — Pop-Out Discoverability & /tell Auto-Pop-Out**\n\n- Pop-out button now visible in the chat header (no more hunting through the right-click menu)\n- One-time hint banner explains pop-out tabs and the right-click shortcut\n- New setting: open new /tell tabs directly as pop-out windows (Settings → Chat → Auto-Tell-Tabs)\n- Pop-out input is now enabled by default — closing a pop-out still returns the tab to the sidebar\n- Bugfix: dropping or logging out with an LRU/popped auto-tell tab now also closes its pop-out window (no more ghost windows)\n- Bugfix: dead zone below the chat input bar when the v0.6.0 pop-out hint banner was visible (also fixed retroactively for the v0.6.0 banner inside pop-outs)\n\nModding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\nEarlier history: https://github.com/JonKazama-Hellion/HellionChat/releases", "AcceptsFeedback": true, "DownloadLinkInstall": "https://github.com/JonKazama-Hellion/HellionChat/releases/download/v1.0.1/latest.zip", "DownloadLinkUpdate": "https://github.com/JonKazama-Hellion/HellionChat/releases/download/v1.0.1/latest.zip", "DownloadLinkTesting": "https://github.com/JonKazama-Hellion/HellionChat/releases/download/v1.0.1/latest.zip", - "TestingAssemblyVersion": "1.0.1.0", + "TestingAssemblyVersion": "1.0.2.0", "IconUrl": "https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/icon.png", "ImageUrls": [ "https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/chatWindow.png", -- 2.52.0 From a3fbaab173bccb436d621d9e9f70a6cb0a68a2b6 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Mon, 4 May 2026 16:14:14 +0200 Subject: [PATCH 014/169] fix(release): bump DownloadLink URLs to v1.0.2 Manifest-bump in 8e9332a missed the three DownloadLink* entries. Plugin-Manager fetched v1.0.1 zip (AssemblyVersion 1.0.1.0) against the new repo.json (AssemblyVersion 1.0.2.0), tripping the version- match guard. --- repo.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/repo.json b/repo.json index 7f5ed56..c2043ed 100644 --- a/repo.json +++ b/repo.json @@ -22,9 +22,9 @@ "Punchline": "Chat replacement with privacy controls aligned to EU, US and JP rules — based on Chat 2 (EUPL-1.2)", "Changelog": "**Hellion Chat 1.0.2 — Polish patch**\n\n- New: optionally hide chat (and every other plugin window) while the New Game+ menu is open. Toggle in Settings → Window → Frame, default off. Closing the menu restores all windows.\n- New: optionally tint the channel selector button next to the input field with the currently active channel's colour. Toggle in Settings → Appearance → Colours, default on. Matches the existing input-text tint and respects ExtraChat overrides.\n- Fix: status, item and other inline hover icons keep their original aspect ratio. Debuff icons with non-square dimensions are no longer visually squished into a 32×32 box.\n- Diagnostic: hide-state transitions (battle, cutscene, user-hide, cutscene override) are now logged on Verbose level for easier bug reports — off by default, enable with `/xllog set HellionChat verbose`.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 1.0.1 — Window Position Recovery**\n\n- Automatic bounds check on the first draw after plugin load. When the persisted window position has no overlap with the primary viewport, the window snaps to a safe top-left default. Helpful after a monitor disconnect, resolution change or multi-monitor layout switch between sessions.\n- New \"Reset Window Position\" button in Settings → Window → Frame as a manual escape hatch for edge cases the automatic check doesn't catch.\n\nTested on Linux/Wayland with a hard-cut three-monitor reduction; window recovers cleanly without manual JSON editing.\n\nHousekeeping carried over since v1.0.0:\n\n- Documentation restructured into docs/ folder. New CHANGELOG, CONTRIBUTORS, LEARNING-JOURNEY and ROADMAP added\n- Stale ChatTwo/* paths in repo configs updated to HellionChat/*\n- Pidgin parser library bumped from 3.3.0 to 3.5.1 (CIString Unicode fix relevant for non-ASCII channel/tab names)\n- GitHub Actions: actions/setup-dotnet bumped 4 → 5, github/codeql-action bumped 3 → 4\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 1.0.0 — Standalone Major Release**\n\nFirst fully standalone release. Internal cleanup plus a sweep of\npre-existing correctness, security, threading and resource-leak\nfixes carried over from the upstream codebase. No user action\nrequired — auto-update applies cleanly, configuration and database\npaths unchanged.\n\nStandalone identity:\n\n- Code namespace consolidated from ChatTwo.* to HellionChat.* across\n all source files\n- IPC channels migrated from ChatTwo.* to HellionChat.* (6 channels:\n Register, Available, Unregister, Invoke, GetChatInputState,\n ChatInputStateChanged) — third-party plugins that bound to the old\n channels need to be updated; none known at release time\n- ImGui popup ID renamed to hellionchat-context-popup\n- Repository folder restructured (ChatTwo/ → HellionChat/), all CI\n and build paths updated accordingly\n- Public-facing descriptions reworded from upstream-fork framing to\n standalone framing (Chat 2 attribution preserved per EUPL-1.2)\n- Colour preset 'ChatTwo Default' is now 'Klassik (Chat 2 Default)'\n\nSafety:\n\n- Plugin now refuses to load when upstream Chat 2 is also active —\n bilingual conflict message in EN/DE, throw before any subsystem\n initialization, prevents the runtime crash that previously occurred\n when both plugins replaced the same chat window in parallel\n- SQLite native binary bumped to 3.50.3 (CVE-2025-6965 memory\n corruption from aggregate-term overflow, CVE-2025-7709)\n- NuGet restore now honors packages.lock.json so transitive\n dependencies don't drift between machines or CI runs\n\nDefault tab layout sharpened (one-time tab reset on first start):\n\nThe first-run tab layout is reorganized into five thematic tabs\nbased on external tester feedback. General contains only Say,\nYell and Shout (immediate-surroundings public chat). System\nabsorbs the gameplay-event streams (NpcDialogue, Loot, Crafting,\nGathering, PF recruitment pings) and announcement noise\n(BattleSystem, FreeCompanyAnnouncement, PvpTeamAnnouncement)\nthat previously lived in General. FreeCompany, Group and\nLinkshell each own their channel set. The static Tell tab is\ngone — Auto-Tell-Tabs spawns per-conversation tabs on demand.\nThe Beginner / Novice-Network preset is no longer added by\ndefault but is still available via Settings, Tabs.\n\nThis is a one-time tab-layout reset for users on config version\n12 or older. Privacy, Retention, Theme and every other setting\nis preserved. Your previous tab configuration is written to\npluginConfigs/HellionChat.json.pre-v13-backup so you can restore\nit manually if you prefer the old layout.\n\nCrash-class fixes (formerly latent in upstream):\n\n- MathUtil.HasOverlap now uses a correct AABB test; identical or\n edge-touching rectangles are no longer reported as non-overlapping\n- ChatCode.Equals compares fields directly instead of GetHashCode;\n removes the hash-collision anti-pattern\n- IpcManager.Dispose uses UnregisterAction to match the matching\n RegisterAction call; previous mismatch leaked the action\n subscription on every plugin reload\n- ExtraChat.Dispose now unsubscribes all three IPC subscriptions\n (was only the first); leaks closed\n- TellTarget.FromTarget guards against a zero IPlayerCharacter.Address\n before dereferencing the unsafe Character* cast\n- GameFunctions ResolveTextCommandPlaceholderDetour null-checks the\n Hook reference instead of using the null-forgiving operator\n- Popout.cs and SettingsTabs/Tabs.cs bounds-check list indexing so\n a tab drop or empty-worlds list no longer crashes the UI\n- Debugger.cs now declares IDisposable so the existing Dispose runs\n\nCorrectness fixes:\n\n- GlobalParametersCache.GetValue captures Cache into a local before\n the bounds check, so a concurrent Refresh can't slip a different\n array between check and read\n- IconUtil binary search bounds initialized to entries.Length-1 and\n reset on redirect-restart; entries.Length==0 short-circuits\n- Sheets.WorldsOnDatacenter now compares DataCenter.RowId (was\n Region.RowId) so it actually returns same-DC worlds\n- Message.cs back-reference loop iterates the processed Sender/Content\n properties so chunks added by CheckMessageContent get Message set\n- Language.zh-Hans Webinterface_Start_Success corrected to\n \"网页界面已启动\" (was \"网页界面已停止\")\n\nThreading and async:\n\n- AutoTranslate Entries/ValidEntries are now serialized behind a\n single lock; the preload worker thread and main thread no longer\n race on the underlying dictionary/hash set\n- Privacy retention and cleanup workers bound their framework-refresh\n waits to 5 seconds with a logged timeout; a hung framework tick can\n no longer deadlock the background worker\n\nResource handling:\n\n- EmoteCache reuses the static HttpClient instead of allocating a new\n one per call (closed socket leak)\n- FontManager wraps HttpClient/HttpResponseMessage in using-blocks\n and adds EnsureSuccessStatusCode; failed downloads no longer\n silently produce a zero-byte font file\n- SearchSelector mixes the row index into the ImGui ID stack so\n selectables don't collapse to a single ambiguous ID\n- SettingsTabs/Chat blocked-emote add-button now opens its selector\n popup on left-click\n\nPerformance:\n\n- DbViewer text export caches filteredHistory.Count once instead of\n re-enumerating the IEnumerable on every batch (O(N) instead of\n O(N²) on large histories)\n\nLicense attribution (NOTICE.md, COPYRIGHT, THIRD_PARTY_NOTICES.md\nand the Credits section in README) is unchanged.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 0.6.1 — Pop-Out Discoverability & /tell Auto-Pop-Out**\n\n- Pop-out button now visible in the chat header (no more hunting through the right-click menu)\n- One-time hint banner explains pop-out tabs and the right-click shortcut\n- New setting: open new /tell tabs directly as pop-out windows (Settings → Chat → Auto-Tell-Tabs)\n- Pop-out input is now enabled by default — closing a pop-out still returns the tab to the sidebar\n- Bugfix: dropping or logging out with an LRU/popped auto-tell tab now also closes its pop-out window (no more ghost windows)\n- Bugfix: dead zone below the chat input bar when the v0.6.0 pop-out hint banner was visible (also fixed retroactively for the v0.6.0 banner inside pop-outs)\n\nModding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\nEarlier history: https://github.com/JonKazama-Hellion/HellionChat/releases", "AcceptsFeedback": true, - "DownloadLinkInstall": "https://github.com/JonKazama-Hellion/HellionChat/releases/download/v1.0.1/latest.zip", - "DownloadLinkUpdate": "https://github.com/JonKazama-Hellion/HellionChat/releases/download/v1.0.1/latest.zip", - "DownloadLinkTesting": "https://github.com/JonKazama-Hellion/HellionChat/releases/download/v1.0.1/latest.zip", + "DownloadLinkInstall": "https://github.com/JonKazama-Hellion/HellionChat/releases/download/v1.0.2/latest.zip", + "DownloadLinkUpdate": "https://github.com/JonKazama-Hellion/HellionChat/releases/download/v1.0.2/latest.zip", + "DownloadLinkTesting": "https://github.com/JonKazama-Hellion/HellionChat/releases/download/v1.0.2/latest.zip", "TestingAssemblyVersion": "1.0.2.0", "IconUrl": "https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/icon.png", "ImageUrls": [ -- 2.52.0 From 698eb01bbe8d7f58c604883e393a44b37c903c16 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Mon, 4 May 2026 16:17:06 +0200 Subject: [PATCH 015/169] chore(release): bump to v1.0.3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v1.0.2 tag was claimed before the DownloadLink fix shipped, so the content moves to v1.0.3. No code changes — manifest, repo.json, CHANGELOG and README version refs roll forward; DownloadLink* URLs now point to v1.0.3/latest.zip. --- HellionChat/HellionChat.csproj | 2 +- HellionChat/HellionChat.yaml | 2 +- README.md | 2 +- docs/CHANGELOG.md | 4 ++-- repo.json | 12 ++++++------ 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/HellionChat/HellionChat.csproj b/HellionChat/HellionChat.csproj index dc7f418..d56f76c 100644 --- a/HellionChat/HellionChat.csproj +++ b/HellionChat/HellionChat.csproj @@ -4,7 +4,7 @@ 0.1.0 is our bootstrap release; the underlying Chat 2 base is called out in the yaml changelog so users can see what it derives from. --> - 1.0.2 + 1.0.3 enable diff --git a/HellionChat/HellionChat.yaml b/HellionChat/HellionChat.yaml index a8b26a9..ba4e13f 100755 --- a/HellionChat/HellionChat.yaml +++ b/HellionChat/HellionChat.yaml @@ -49,7 +49,7 @@ tags: - Replacement - Privacy changelog: |- - **Hellion Chat 1.0.2 — Polish patch** + **Hellion Chat 1.0.3 — Polish patch** - New: optionally hide chat (and every other plugin window) while the New Game+ menu is open. Toggle in Settings → Window → Frame, default diff --git a/README.md b/README.md index 95e121b..f6a898b 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![.NET](https://img.shields.io/badge/.NET-10.0-512BD4)](https://dotnet.microsoft.com/) [![FFXIV](https://img.shields.io/badge/FFXIV-Dawntrail-c3a37f)](https://www.finalfantasyxiv.com/) -**Version 1.0.2** — DSGVO-bewusster Chat-Ersatz für FINAL FANTASY XIV / Dalamud, basierend auf [Chat 2](https://github.com/Infiziert90/ChatTwo) (EUPL-1.2). +**Version 1.0.3** — DSGVO-bewusster Chat-Ersatz für FINAL FANTASY XIV / Dalamud, basierend auf [Chat 2](https://github.com/Infiziert90/ChatTwo) (EUPL-1.2). Hellion Chat ergänzt das ursprüngliche Chat-2-Fundament um Datenschutz- und Daten-Handling-Kontrollen, die mit den Datenschutz-Regeln in der EU, den USA und Japan im Einklang sind. Alle aus Chat 2 übernommenen Funktionen, Befehle und Tastenkürzel funktionieren unverändert. Eigenständiger Plugin-Slot, eigene Konfiguration, eigene Datenbank. diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 15b37b7..80f1320 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -12,7 +12,7 @@ und verlinkt für Details auf die Release-Pages. --- -## [1.0.2] — 2026-05-04 — Polish patch +## [1.0.3] — 2026-05-04 — Polish patch Vier kleine Polish-Items aus dem Backlog gebündelt: @@ -37,7 +37,7 @@ Vier kleine Polish-Items aus dem Backlog gebündelt: auf Verbose-Level. Aus by default, Aktivierung via `/xllog set HellionChat verbose` für Bug-Report-Diagnose. -[Release-Notes 1.0.2](https://github.com/JonKazama-Hellion/HellionChat/releases/tag/v1.0.2) +[Release-Notes 1.0.3](https://github.com/JonKazama-Hellion/HellionChat/releases/tag/v1.0.3) ## [1.0.1] — 2026-05-04 — Window Position Recovery diff --git a/repo.json b/repo.json index c2043ed..7c255a6 100644 --- a/repo.json +++ b/repo.json @@ -3,7 +3,7 @@ "Author": "JonKazama-Hellion", "Name": "Hellion Chat", "InternalName": "HellionChat", - "AssemblyVersion": "1.0.2.0", + "AssemblyVersion": "1.0.3.0", "Description": "Hellion Chat is a privacy-focused chat replacement for FINAL FANTASY XIV based on the Chat 2 codebase (EUPL-1.2). One feature is intentionally removed (the optional webinterface) and a stack of privacy controls is added on top. Tabs, channel filters, RGB colours, emotes, screenshot mode, IPC integration and the chat replacement window itself work the same. The webinterface is intentionally not part of Hellion Chat because it serves a different use case from the smaller default footprint this plugin is built around.\n\nOn top of that, Hellion Chat adds privacy and data-handling controls designed to align with the modern data protection rules that apply across the EU, the United States and Japan. By default only your own conversations are stored; messages from strangers, NPCs and system spam stay out of the database. Retention windows are configurable per channel, history can be wiped retroactively, and stored data can be exported on demand.\n\nKey privacy and data-handling features:\n\n- Channel whitelist with a Privacy-First default\n- Per-channel retention with a daily background sweep\n- Retroactive cleanup with a Ctrl+Shift confirm\n- Export to Markdown, JSON or CSV\n- First-run wizard with three preset profiles (Privacy-First, Casual, Full History)\n- Bilingual UI (English and German) with live language switching\n- Independent plugin state — own config file and database directory, so Hellion Chat does not share state with upstream Chat 2\n\nBased on Chat 2 by Infi and Anna, licensed under EUPL-1.2.\n\nModding & support: join the Hellion Forge Discord at https://discord.gg/X9V7Kcv5gR — community for Hellion Chat and other Hellion Online Media plugins/tools.", "ApplicableVersion": "any", "RepoUrl": "https://github.com/JonKazama-Hellion/HellionChat", @@ -20,12 +20,12 @@ "CanUnloadAsync": false, "LoadPriority": 0, "Punchline": "Chat replacement with privacy controls aligned to EU, US and JP rules — based on Chat 2 (EUPL-1.2)", - "Changelog": "**Hellion Chat 1.0.2 — Polish patch**\n\n- New: optionally hide chat (and every other plugin window) while the New Game+ menu is open. Toggle in Settings → Window → Frame, default off. Closing the menu restores all windows.\n- New: optionally tint the channel selector button next to the input field with the currently active channel's colour. Toggle in Settings → Appearance → Colours, default on. Matches the existing input-text tint and respects ExtraChat overrides.\n- Fix: status, item and other inline hover icons keep their original aspect ratio. Debuff icons with non-square dimensions are no longer visually squished into a 32×32 box.\n- Diagnostic: hide-state transitions (battle, cutscene, user-hide, cutscene override) are now logged on Verbose level for easier bug reports — off by default, enable with `/xllog set HellionChat verbose`.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 1.0.1 — Window Position Recovery**\n\n- Automatic bounds check on the first draw after plugin load. When the persisted window position has no overlap with the primary viewport, the window snaps to a safe top-left default. Helpful after a monitor disconnect, resolution change or multi-monitor layout switch between sessions.\n- New \"Reset Window Position\" button in Settings → Window → Frame as a manual escape hatch for edge cases the automatic check doesn't catch.\n\nTested on Linux/Wayland with a hard-cut three-monitor reduction; window recovers cleanly without manual JSON editing.\n\nHousekeeping carried over since v1.0.0:\n\n- Documentation restructured into docs/ folder. New CHANGELOG, CONTRIBUTORS, LEARNING-JOURNEY and ROADMAP added\n- Stale ChatTwo/* paths in repo configs updated to HellionChat/*\n- Pidgin parser library bumped from 3.3.0 to 3.5.1 (CIString Unicode fix relevant for non-ASCII channel/tab names)\n- GitHub Actions: actions/setup-dotnet bumped 4 → 5, github/codeql-action bumped 3 → 4\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 1.0.0 — Standalone Major Release**\n\nFirst fully standalone release. Internal cleanup plus a sweep of\npre-existing correctness, security, threading and resource-leak\nfixes carried over from the upstream codebase. No user action\nrequired — auto-update applies cleanly, configuration and database\npaths unchanged.\n\nStandalone identity:\n\n- Code namespace consolidated from ChatTwo.* to HellionChat.* across\n all source files\n- IPC channels migrated from ChatTwo.* to HellionChat.* (6 channels:\n Register, Available, Unregister, Invoke, GetChatInputState,\n ChatInputStateChanged) — third-party plugins that bound to the old\n channels need to be updated; none known at release time\n- ImGui popup ID renamed to hellionchat-context-popup\n- Repository folder restructured (ChatTwo/ → HellionChat/), all CI\n and build paths updated accordingly\n- Public-facing descriptions reworded from upstream-fork framing to\n standalone framing (Chat 2 attribution preserved per EUPL-1.2)\n- Colour preset 'ChatTwo Default' is now 'Klassik (Chat 2 Default)'\n\nSafety:\n\n- Plugin now refuses to load when upstream Chat 2 is also active —\n bilingual conflict message in EN/DE, throw before any subsystem\n initialization, prevents the runtime crash that previously occurred\n when both plugins replaced the same chat window in parallel\n- SQLite native binary bumped to 3.50.3 (CVE-2025-6965 memory\n corruption from aggregate-term overflow, CVE-2025-7709)\n- NuGet restore now honors packages.lock.json so transitive\n dependencies don't drift between machines or CI runs\n\nDefault tab layout sharpened (one-time tab reset on first start):\n\nThe first-run tab layout is reorganized into five thematic tabs\nbased on external tester feedback. General contains only Say,\nYell and Shout (immediate-surroundings public chat). System\nabsorbs the gameplay-event streams (NpcDialogue, Loot, Crafting,\nGathering, PF recruitment pings) and announcement noise\n(BattleSystem, FreeCompanyAnnouncement, PvpTeamAnnouncement)\nthat previously lived in General. FreeCompany, Group and\nLinkshell each own their channel set. The static Tell tab is\ngone — Auto-Tell-Tabs spawns per-conversation tabs on demand.\nThe Beginner / Novice-Network preset is no longer added by\ndefault but is still available via Settings, Tabs.\n\nThis is a one-time tab-layout reset for users on config version\n12 or older. Privacy, Retention, Theme and every other setting\nis preserved. Your previous tab configuration is written to\npluginConfigs/HellionChat.json.pre-v13-backup so you can restore\nit manually if you prefer the old layout.\n\nCrash-class fixes (formerly latent in upstream):\n\n- MathUtil.HasOverlap now uses a correct AABB test; identical or\n edge-touching rectangles are no longer reported as non-overlapping\n- ChatCode.Equals compares fields directly instead of GetHashCode;\n removes the hash-collision anti-pattern\n- IpcManager.Dispose uses UnregisterAction to match the matching\n RegisterAction call; previous mismatch leaked the action\n subscription on every plugin reload\n- ExtraChat.Dispose now unsubscribes all three IPC subscriptions\n (was only the first); leaks closed\n- TellTarget.FromTarget guards against a zero IPlayerCharacter.Address\n before dereferencing the unsafe Character* cast\n- GameFunctions ResolveTextCommandPlaceholderDetour null-checks the\n Hook reference instead of using the null-forgiving operator\n- Popout.cs and SettingsTabs/Tabs.cs bounds-check list indexing so\n a tab drop or empty-worlds list no longer crashes the UI\n- Debugger.cs now declares IDisposable so the existing Dispose runs\n\nCorrectness fixes:\n\n- GlobalParametersCache.GetValue captures Cache into a local before\n the bounds check, so a concurrent Refresh can't slip a different\n array between check and read\n- IconUtil binary search bounds initialized to entries.Length-1 and\n reset on redirect-restart; entries.Length==0 short-circuits\n- Sheets.WorldsOnDatacenter now compares DataCenter.RowId (was\n Region.RowId) so it actually returns same-DC worlds\n- Message.cs back-reference loop iterates the processed Sender/Content\n properties so chunks added by CheckMessageContent get Message set\n- Language.zh-Hans Webinterface_Start_Success corrected to\n \"网页界面已启动\" (was \"网页界面已停止\")\n\nThreading and async:\n\n- AutoTranslate Entries/ValidEntries are now serialized behind a\n single lock; the preload worker thread and main thread no longer\n race on the underlying dictionary/hash set\n- Privacy retention and cleanup workers bound their framework-refresh\n waits to 5 seconds with a logged timeout; a hung framework tick can\n no longer deadlock the background worker\n\nResource handling:\n\n- EmoteCache reuses the static HttpClient instead of allocating a new\n one per call (closed socket leak)\n- FontManager wraps HttpClient/HttpResponseMessage in using-blocks\n and adds EnsureSuccessStatusCode; failed downloads no longer\n silently produce a zero-byte font file\n- SearchSelector mixes the row index into the ImGui ID stack so\n selectables don't collapse to a single ambiguous ID\n- SettingsTabs/Chat blocked-emote add-button now opens its selector\n popup on left-click\n\nPerformance:\n\n- DbViewer text export caches filteredHistory.Count once instead of\n re-enumerating the IEnumerable on every batch (O(N) instead of\n O(N²) on large histories)\n\nLicense attribution (NOTICE.md, COPYRIGHT, THIRD_PARTY_NOTICES.md\nand the Credits section in README) is unchanged.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 0.6.1 — Pop-Out Discoverability & /tell Auto-Pop-Out**\n\n- Pop-out button now visible in the chat header (no more hunting through the right-click menu)\n- One-time hint banner explains pop-out tabs and the right-click shortcut\n- New setting: open new /tell tabs directly as pop-out windows (Settings → Chat → Auto-Tell-Tabs)\n- Pop-out input is now enabled by default — closing a pop-out still returns the tab to the sidebar\n- Bugfix: dropping or logging out with an LRU/popped auto-tell tab now also closes its pop-out window (no more ghost windows)\n- Bugfix: dead zone below the chat input bar when the v0.6.0 pop-out hint banner was visible (also fixed retroactively for the v0.6.0 banner inside pop-outs)\n\nModding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\nEarlier history: https://github.com/JonKazama-Hellion/HellionChat/releases", + "Changelog": "**Hellion Chat 1.0.3 — Polish patch**\n\n- New: optionally hide chat (and every other plugin window) while the New Game+ menu is open. Toggle in Settings → Window → Frame, default off. Closing the menu restores all windows.\n- New: optionally tint the channel selector button next to the input field with the currently active channel's colour. Toggle in Settings → Appearance → Colours, default on. Matches the existing input-text tint and respects ExtraChat overrides.\n- Fix: status, item and other inline hover icons keep their original aspect ratio. Debuff icons with non-square dimensions are no longer visually squished into a 32×32 box.\n- Diagnostic: hide-state transitions (battle, cutscene, user-hide, cutscene override) are now logged on Verbose level for easier bug reports — off by default, enable with `/xllog set HellionChat verbose`.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 1.0.1 — Window Position Recovery**\n\n- Automatic bounds check on the first draw after plugin load. When the persisted window position has no overlap with the primary viewport, the window snaps to a safe top-left default. Helpful after a monitor disconnect, resolution change or multi-monitor layout switch between sessions.\n- New \"Reset Window Position\" button in Settings → Window → Frame as a manual escape hatch for edge cases the automatic check doesn't catch.\n\nTested on Linux/Wayland with a hard-cut three-monitor reduction; window recovers cleanly without manual JSON editing.\n\nHousekeeping carried over since v1.0.0:\n\n- Documentation restructured into docs/ folder. New CHANGELOG, CONTRIBUTORS, LEARNING-JOURNEY and ROADMAP added\n- Stale ChatTwo/* paths in repo configs updated to HellionChat/*\n- Pidgin parser library bumped from 3.3.0 to 3.5.1 (CIString Unicode fix relevant for non-ASCII channel/tab names)\n- GitHub Actions: actions/setup-dotnet bumped 4 → 5, github/codeql-action bumped 3 → 4\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 1.0.0 — Standalone Major Release**\n\nFirst fully standalone release. Internal cleanup plus a sweep of\npre-existing correctness, security, threading and resource-leak\nfixes carried over from the upstream codebase. No user action\nrequired — auto-update applies cleanly, configuration and database\npaths unchanged.\n\nStandalone identity:\n\n- Code namespace consolidated from ChatTwo.* to HellionChat.* across\n all source files\n- IPC channels migrated from ChatTwo.* to HellionChat.* (6 channels:\n Register, Available, Unregister, Invoke, GetChatInputState,\n ChatInputStateChanged) — third-party plugins that bound to the old\n channels need to be updated; none known at release time\n- ImGui popup ID renamed to hellionchat-context-popup\n- Repository folder restructured (ChatTwo/ → HellionChat/), all CI\n and build paths updated accordingly\n- Public-facing descriptions reworded from upstream-fork framing to\n standalone framing (Chat 2 attribution preserved per EUPL-1.2)\n- Colour preset 'ChatTwo Default' is now 'Klassik (Chat 2 Default)'\n\nSafety:\n\n- Plugin now refuses to load when upstream Chat 2 is also active —\n bilingual conflict message in EN/DE, throw before any subsystem\n initialization, prevents the runtime crash that previously occurred\n when both plugins replaced the same chat window in parallel\n- SQLite native binary bumped to 3.50.3 (CVE-2025-6965 memory\n corruption from aggregate-term overflow, CVE-2025-7709)\n- NuGet restore now honors packages.lock.json so transitive\n dependencies don't drift between machines or CI runs\n\nDefault tab layout sharpened (one-time tab reset on first start):\n\nThe first-run tab layout is reorganized into five thematic tabs\nbased on external tester feedback. General contains only Say,\nYell and Shout (immediate-surroundings public chat). System\nabsorbs the gameplay-event streams (NpcDialogue, Loot, Crafting,\nGathering, PF recruitment pings) and announcement noise\n(BattleSystem, FreeCompanyAnnouncement, PvpTeamAnnouncement)\nthat previously lived in General. FreeCompany, Group and\nLinkshell each own their channel set. The static Tell tab is\ngone — Auto-Tell-Tabs spawns per-conversation tabs on demand.\nThe Beginner / Novice-Network preset is no longer added by\ndefault but is still available via Settings, Tabs.\n\nThis is a one-time tab-layout reset for users on config version\n12 or older. Privacy, Retention, Theme and every other setting\nis preserved. Your previous tab configuration is written to\npluginConfigs/HellionChat.json.pre-v13-backup so you can restore\nit manually if you prefer the old layout.\n\nCrash-class fixes (formerly latent in upstream):\n\n- MathUtil.HasOverlap now uses a correct AABB test; identical or\n edge-touching rectangles are no longer reported as non-overlapping\n- ChatCode.Equals compares fields directly instead of GetHashCode;\n removes the hash-collision anti-pattern\n- IpcManager.Dispose uses UnregisterAction to match the matching\n RegisterAction call; previous mismatch leaked the action\n subscription on every plugin reload\n- ExtraChat.Dispose now unsubscribes all three IPC subscriptions\n (was only the first); leaks closed\n- TellTarget.FromTarget guards against a zero IPlayerCharacter.Address\n before dereferencing the unsafe Character* cast\n- GameFunctions ResolveTextCommandPlaceholderDetour null-checks the\n Hook reference instead of using the null-forgiving operator\n- Popout.cs and SettingsTabs/Tabs.cs bounds-check list indexing so\n a tab drop or empty-worlds list no longer crashes the UI\n- Debugger.cs now declares IDisposable so the existing Dispose runs\n\nCorrectness fixes:\n\n- GlobalParametersCache.GetValue captures Cache into a local before\n the bounds check, so a concurrent Refresh can't slip a different\n array between check and read\n- IconUtil binary search bounds initialized to entries.Length-1 and\n reset on redirect-restart; entries.Length==0 short-circuits\n- Sheets.WorldsOnDatacenter now compares DataCenter.RowId (was\n Region.RowId) so it actually returns same-DC worlds\n- Message.cs back-reference loop iterates the processed Sender/Content\n properties so chunks added by CheckMessageContent get Message set\n- Language.zh-Hans Webinterface_Start_Success corrected to\n \"网页界面已启动\" (was \"网页界面已停止\")\n\nThreading and async:\n\n- AutoTranslate Entries/ValidEntries are now serialized behind a\n single lock; the preload worker thread and main thread no longer\n race on the underlying dictionary/hash set\n- Privacy retention and cleanup workers bound their framework-refresh\n waits to 5 seconds with a logged timeout; a hung framework tick can\n no longer deadlock the background worker\n\nResource handling:\n\n- EmoteCache reuses the static HttpClient instead of allocating a new\n one per call (closed socket leak)\n- FontManager wraps HttpClient/HttpResponseMessage in using-blocks\n and adds EnsureSuccessStatusCode; failed downloads no longer\n silently produce a zero-byte font file\n- SearchSelector mixes the row index into the ImGui ID stack so\n selectables don't collapse to a single ambiguous ID\n- SettingsTabs/Chat blocked-emote add-button now opens its selector\n popup on left-click\n\nPerformance:\n\n- DbViewer text export caches filteredHistory.Count once instead of\n re-enumerating the IEnumerable on every batch (O(N) instead of\n O(N²) on large histories)\n\nLicense attribution (NOTICE.md, COPYRIGHT, THIRD_PARTY_NOTICES.md\nand the Credits section in README) is unchanged.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n**Hellion Chat 0.6.1 — Pop-Out Discoverability & /tell Auto-Pop-Out**\n\n- Pop-out button now visible in the chat header (no more hunting through the right-click menu)\n- One-time hint banner explains pop-out tabs and the right-click shortcut\n- New setting: open new /tell tabs directly as pop-out windows (Settings → Chat → Auto-Tell-Tabs)\n- Pop-out input is now enabled by default — closing a pop-out still returns the tab to the sidebar\n- Bugfix: dropping or logging out with an LRU/popped auto-tell tab now also closes its pop-out window (no more ghost windows)\n- Bugfix: dead zone below the chat input bar when the v0.6.0 pop-out hint banner was visible (also fixed retroactively for the v0.6.0 banner inside pop-outs)\n\nModding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\nEarlier history: https://github.com/JonKazama-Hellion/HellionChat/releases", "AcceptsFeedback": true, - "DownloadLinkInstall": "https://github.com/JonKazama-Hellion/HellionChat/releases/download/v1.0.2/latest.zip", - "DownloadLinkUpdate": "https://github.com/JonKazama-Hellion/HellionChat/releases/download/v1.0.2/latest.zip", - "DownloadLinkTesting": "https://github.com/JonKazama-Hellion/HellionChat/releases/download/v1.0.2/latest.zip", - "TestingAssemblyVersion": "1.0.2.0", + "DownloadLinkInstall": "https://github.com/JonKazama-Hellion/HellionChat/releases/download/v1.0.3/latest.zip", + "DownloadLinkUpdate": "https://github.com/JonKazama-Hellion/HellionChat/releases/download/v1.0.3/latest.zip", + "DownloadLinkTesting": "https://github.com/JonKazama-Hellion/HellionChat/releases/download/v1.0.3/latest.zip", + "TestingAssemblyVersion": "1.0.3.0", "IconUrl": "https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/icon.png", "ImageUrls": [ "https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/chatWindow.png", -- 2.52.0 From 4d54eabdacab2149f607776a6f111500810cd9b4 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 07:25:47 +0200 Subject: [PATCH 016/169] chore: code quality sweep 2026-05-04 / 2026-05-05 General code-quality and robustness pass across the plugin: thread- safety on IPC state, resource-disposal cleanups, input validation, defensive null-checks and a few small UX glitches. Compliance docs (THIRD_PARTY_NOTICES, PRIVACY, COPYRIGHT) refreshed to v1.0.3. Highlights - ExtraChat IPC state synchronised across threads - ChatLogWindow autocomplete no longer leaks the unmanaged ImGuiListClipper allocation - ChatLogWindow + Popout style stack stays balanced when config toggles mid-frame - Retention sweep and privacy cleanup wait for the actual filter pass instead of the fire-and-forget Task that started it - Configuration.LatestVersion bumped to 13 to match the active migration path - GameFunctions placeholder buffer guarded against oversized replacement names - TellTarget.IsSet, ResolveTempInputChannel, InputPreview, IconUtil, Lender, Payloads, ExtraPayload all hardened against null / empty / EOF / cycle inputs - FontManager Lodestone download stays in scope for a follow-up (timeout + lazy init pending) - AutoTranslate replaced the msvcrt.dll memcmp P/Invoke with a managed Span comparison - Privacy cleanup worker thread marked IsBackground = true - Database cleanup now removes both legacy files in one click - Tell-target name redacted in the verbose debug log Compliance - THIRD_PARTY_NOTICES: last-reviewed bumped to v1.0.3, Pidgin 3.5.1, SQLitePCLRaw.lib.e_sqlite3 3.50.3 listed as direct dependency with CVE-2025-6965 / CVE-2025-7709 rationale - PRIVACY: last-reviewed bumped to v1.0.3, BetterTTV trigger wording clarified (list fetch at startup vs. on-demand image fetch) - COPYRIGHT: upstream attribution range widened Build: 0 warnings, 0 errors. No behavioural changes that would alter existing user configuration or stored chat history. --- COPYRIGHT | 2 +- HellionChat/Configuration.cs | 7 +- HellionChat/GameFunctions/Chat.cs | 6 +- HellionChat/GameFunctions/GameFunctions.cs | 14 +- HellionChat/GameFunctions/Types/TellTarget.cs | 2 +- HellionChat/Ipc/ExtraChat.cs | 13 +- HellionChat/MessageManager.cs | 4 + HellionChat/Plugin.cs | 7 +- HellionChat/Ui/ChatInputBar.cs | 2 +- HellionChat/Ui/ChatLogWindow.cs | 142 +++++++++++------- HellionChat/Ui/CommandHelpWindow.cs | 7 +- HellionChat/Ui/InputPreview.cs | 5 +- HellionChat/Ui/Popout.cs | 21 ++- HellionChat/Ui/SeStringDebugger.cs | 11 +- HellionChat/Ui/SettingsTabs/Chat.cs | 10 +- HellionChat/Ui/SettingsTabs/Database.cs | 4 +- HellionChat/Ui/SettingsTabs/Privacy.cs | 13 +- HellionChat/Util/AutoTranslate.cs | 9 +- HellionChat/Util/ExtraPayload.cs | 2 + HellionChat/Util/IconUtil.cs | 25 ++- HellionChat/Util/Lender.cs | 2 +- HellionChat/Util/MemoryUtil.cs | 13 ++ HellionChat/Util/Payloads.cs | 2 + HellionChat/Util/StringUtil.cs | 4 +- PRIVACY.md | 15 +- docs/THIRD_PARTY_NOTICES.md | 7 +- 26 files changed, 251 insertions(+), 98 deletions(-) diff --git a/COPYRIGHT b/COPYRIGHT index 410b3d1..ebd16d4 100644 --- a/COPYRIGHT +++ b/COPYRIGHT @@ -1,6 +1,6 @@ HellionChat — a privacy-focused fork of ChatTwo for FINAL FANTASY XIV -Copyright (c) 2024-2025 Infiziert90 (Infi) and Anna Clemens (ascclemens) +Copyright (c) 2022-2026 Infiziert90 (Infi) and Anna Clemens (ascclemens) Original ChatTwo authors and copyright holders of the upstream plugin this fork is built on. Their work covers the message store, the channel filtering, the sidebar tab system, the FFXIV chat diff --git a/HellionChat/Configuration.cs b/HellionChat/Configuration.cs index f1f9c85..fbe7fdc 100755 --- a/HellionChat/Configuration.cs +++ b/HellionChat/Configuration.cs @@ -34,7 +34,7 @@ public class ConfigKeyBind [Serializable] public class Configuration : IPluginConfiguration { - private const int LatestVersion = 12; + private const int LatestVersion = 13; public int Version { get; set; } = LatestVersion; @@ -279,7 +279,10 @@ public class Configuration : IPluginConfiguration MaxLinesToRender = other.MaxLinesToRender; Use24HourClock = other.Use24HourClock; ShowEmotes = other.ShowEmotes; - BlockedEmotes = other.BlockedEmotes; + // Deep-copy the set so the live and mutable Configuration instances don't share state + // — a HashSet reference assignment would cause edits in the settings window to leak + // into the live config before the user clicks Save. + BlockedEmotes = new HashSet(other.BlockedEmotes); FontsEnabled = other.FontsEnabled; ItalicEnabled = other.ItalicEnabled; ExtraGlyphRanges = other.ExtraGlyphRanges; diff --git a/HellionChat/GameFunctions/Chat.cs b/HellionChat/GameFunctions/Chat.cs index 20790ab..4c714c7 100755 --- a/HellionChat/GameFunctions/Chat.cs +++ b/HellionChat/GameFunctions/Chat.cs @@ -252,7 +252,7 @@ internal sealed unsafe class Chat : IDisposable { playerName = SeString.Parse(agent->TellPlayerName).TextValue; worldId = agent->TellWorldId; - Plugin.Log.Debug($"Detected tell target '{playerName}'@{worldId}"); + Plugin.Log.Debug($"Detected tell target '[redacted]'@{worldId}"); } Plugin.CurrentTab.CurrentChannel = new UsedChannel @@ -400,7 +400,9 @@ internal sealed unsafe class Chat : IDisposable } var idx = RotateLinkshell(currentIndex, rotate, channel == InputChannel.Linkshell1 ? ValidLinkshell : ValidCrossLinkshell); - return channel + idx; + // RotateLinkshell returns null when no valid linkshell is found within 8 iterations. + // Forward the null so the caller can keep the existing channel instead of crashing on nullable arithmetic. + return idx is null ? null : channel + idx.Value; } default: return channel; diff --git a/HellionChat/GameFunctions/GameFunctions.cs b/HellionChat/GameFunctions/GameFunctions.cs index c05b8f7..b85bb72 100755 --- a/HellionChat/GameFunctions/GameFunctions.cs +++ b/HellionChat/GameFunctions/GameFunctions.cs @@ -245,7 +245,8 @@ internal unsafe class GameFunctions : IDisposable vf0(agent, &result, &value, 0, 0); } - private readonly nint PlaceholderNamePtr = Marshal.AllocHGlobal(128); + private const int PlaceholderBufferSize = 128; + private readonly nint PlaceholderNamePtr = Marshal.AllocHGlobal(PlaceholderBufferSize); private readonly string Placeholder = $"<{Guid.NewGuid():N}>"; private string? ReplacementName; @@ -261,6 +262,17 @@ internal unsafe class GameFunctions : IDisposable if (ReplacementName == null || placeholder != Placeholder) return ResolveTextCommandPlaceholderHook.Original(a1, placeholderText, a3, a4); + // The fixed buffer is 128 bytes; UTF-8 + null-terminator must fit. + // FFXIV player names plus an @World suffix should never approach this + // limit, but a malformed ReplacementName must not overflow the buffer. + var byteCount = System.Text.Encoding.UTF8.GetByteCount(ReplacementName); + if (byteCount >= PlaceholderBufferSize) + { + Plugin.Log.Warning($"Replacement name too long for placeholder buffer ({byteCount} bytes >= {PlaceholderBufferSize}); falling back to original."); + ReplacementName = null; + return ResolveTextCommandPlaceholderHook.Original(a1, placeholderText, a3, a4); + } + MemoryHelper.WriteString(PlaceholderNamePtr, ReplacementName); ReplacementName = null; diff --git a/HellionChat/GameFunctions/Types/TellTarget.cs b/HellionChat/GameFunctions/Types/TellTarget.cs index b6151c2..554dbd2 100755 --- a/HellionChat/GameFunctions/Types/TellTarget.cs +++ b/HellionChat/GameFunctions/Types/TellTarget.cs @@ -20,7 +20,7 @@ public class TellTarget } public bool IsSet() - => Name.Length > 0 && World > 0; + => !string.IsNullOrEmpty(Name) && World > 0; public string ToWorldString() => Sheets.WorldSheet.TryGetRow(World, out var worldRow) ? worldRow.Name.ToString() : string.Empty; diff --git a/HellionChat/Ipc/ExtraChat.cs b/HellionChat/Ipc/ExtraChat.cs index b04521e..c51ac54 100644 --- a/HellionChat/Ipc/ExtraChat.cs +++ b/HellionChat/Ipc/ExtraChat.cs @@ -20,10 +20,14 @@ public sealed class ExtraChat : IDisposable internal (string, uint)? ChannelOverride { get; set; } - private Dictionary ChannelCommandColoursInternal { get; set; } = new(); + // Volatile reference: IPC callbacks (OnChannelCommandColours/OnChannelNames) fire on a + // Dalamud-dispatcher thread while the ImGui thread reads the IReadOnlyDictionary projections. + // Reference assignment is atomic on x64, but the JIT (especially Mono on Wine/Linux) needs + // the volatile barrier to guarantee visibility across threads. See AUDIT-2026-05-05 [SEC-01]. + private volatile Dictionary ChannelCommandColoursInternal = new(); internal IReadOnlyDictionary ChannelCommandColours => ChannelCommandColoursInternal; - private Dictionary ChannelNamesInternal { get; set; } = new(); + private volatile Dictionary ChannelNamesInternal = new(); internal IReadOnlyDictionary ChannelNames => ChannelNamesInternal; internal ExtraChat() @@ -40,9 +44,10 @@ public sealed class ExtraChat : IDisposable ChannelCommandColoursInternal = ChannelCommandColoursGate.InvokeFunc(null!); ChannelNamesInternal = ChannelNamesGate.InvokeFunc(null!); } - catch (Exception) + catch (Exception ex) { - // no-op + // ExtraChat is optional; missing IPC peer is normal when the plugin isn't loaded. + Plugin.Log.Verbose(ex, "ExtraChat IPC initial state query failed (peer not loaded?)"); } } diff --git a/HellionChat/MessageManager.cs b/HellionChat/MessageManager.cs index d4b1fe4..a7b60e5 100644 --- a/HellionChat/MessageManager.cs +++ b/HellionChat/MessageManager.cs @@ -93,6 +93,10 @@ internal class MessageManager : IAsyncDisposable Plugin.Log.Debug("Sleeping because PendingMessageThread thread still alive"); } + // CancellationTokenSource owns an unmanaged WaitHandle; dispose after the + // worker thread has drained, otherwise it leaks across plugin reloads. + PendingThreadCancellationToken.Dispose(); + Store.Dispose(); } diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index 62052c9..992600a 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -529,10 +529,15 @@ public sealed class Plugin : IDalamudPlugin if (deleted > 0) { Log.Information($"Retention sweep deleted {deleted} expired messages."); + // Run the clear+refilter synchronously on the framework thread. + // Earlier this called FilterAllTabsAsync(), which is fire-and-forget + // — the .Wait() here would return as soon as the inner Task.Run was + // dispatched, racing the next sweep cycle against the still-running + // filter pass. See AUDIT-2026-05-05 [QUAL-02]. Framework.Run(() => { MessageManager.ClearAllTabs(); - MessageManager.FilterAllTabsAsync(); + MessageManager.FilterAllTabs(); }).Wait(); } else diff --git a/HellionChat/Ui/ChatInputBar.cs b/HellionChat/Ui/ChatInputBar.cs index b2319ea..f1280a9 100644 --- a/HellionChat/Ui/ChatInputBar.cs +++ b/HellionChat/Ui/ChatInputBar.cs @@ -104,7 +104,7 @@ public sealed class ChatInputBar // window's logic but operates on _state.HistoryCursor and the shared // InputHistoryService. Index semantics match v0.5.x InputBacklog: // 0 = oldest, Count-1 = newest. - private unsafe int CompactCallback(scoped ref ImGuiInputTextCallbackData data) + private int CompactCallback(scoped ref ImGuiInputTextCallbackData data) { if (data.EventFlag != ImGuiInputTextFlags.CallbackHistory) return 0; diff --git a/HellionChat/Ui/ChatLogWindow.cs b/HellionChat/Ui/ChatLogWindow.cs index ebeb8dc..cd7c8db 100644 --- a/HellionChat/Ui/ChatLogWindow.cs +++ b/HellionChat/Ui/ChatLogWindow.cs @@ -34,6 +34,9 @@ public sealed class ChatLogWindow : Window internal Plugin Plugin { get; } + private readonly CommandWrapper _clearHellionCommand; + private readonly CommandWrapper _hellionCommand; + internal bool ScreenshotMode; private string Salt { get; } @@ -110,8 +113,14 @@ public sealed class ChatLogWindow : Window SetUpTextCommandChannels(); SetUpAllCommands(); - Plugin.Commands.Register("/clearhellion", "Clear the Hellion Chat log").Execute += ClearLog; - Plugin.Commands.Register("/hellion").Execute += ToggleChat; + // Cache the registered wrapper instances so Dispose can detach the same + // event objects the constructor attached to, without going through + // Register() again (which would re-create the wrapper if the command + // happened to be missing from the dictionary). + _clearHellionCommand = Plugin.Commands.Register("/clearhellion", "Clear the Hellion Chat log"); + _hellionCommand = Plugin.Commands.Register("/hellion"); + _clearHellionCommand.Execute += ClearLog; + _hellionCommand.Execute += ToggleChat; Plugin.ClientState.Login += Login; Plugin.ClientState.Logout += Logout; @@ -126,8 +135,8 @@ public sealed class ChatLogWindow : Window Plugin.AddonLifecycle.UnregisterListener(AddonEvent.PostUpdate, "ActionDetail", PayloadHandler.MoveTooltip); Plugin.ClientState.Logout -= Logout; Plugin.ClientState.Login -= Login; - Plugin.Commands.Register("/hellion").Execute -= ToggleChat; - Plugin.Commands.Register("/clearhellion").Execute -= ClearLog; + _hellionCommand.Execute -= ToggleChat; + _clearHellionCommand.Execute -= ClearLog; } private void Logout(int _, int __) @@ -514,13 +523,28 @@ public sealed class ChatLogWindow : Window return FrameTime - lastActivityTime <= 1000 * Plugin.Config.InactivityHideTimeout; } + // Tracks the style instance pushed in PreDraw so PostDraw can pop the same + // one even if the user toggled OverrideStyle / ChosenStyle mid-frame. + // Without this, a config change between PreDraw and PostDraw could either + // leak a Push (no matching Pop) or pop nothing while we still have a frame + // pushed onto the ImGui stack. + private StyleModel? _pushedStyle; + public override void PreDraw() { if (Plugin.Config.KeepInputFocus && Activate) ImGui.SetWindowFocus(WindowName); + _pushedStyle = null; if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null }) - StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == Plugin.Config.ChosenStyle)?.Push(); + { + var style = StyleModel.GetConfiguredStyles()?.FirstOrDefault(s => s.Name == Plugin.Config.ChosenStyle); + if (style != null) + { + style.Push(); + _pushedStyle = style; + } + } } public override void PostDraw() @@ -532,8 +556,11 @@ public sealed class ChatLogWindow : Window if (Plugin.CurrentTab.InputDisabled) Activate = false; - if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null }) - StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == Plugin.Config.ChosenStyle)?.Pop(); + if (_pushedStyle != null) + { + _pushedStyle.Pop(); + _pushedStyle = null; + } } public override void OnClose() @@ -597,10 +624,11 @@ public sealed class ChatLogWindow : Window Plugin.InputPreview.CalculatePreview(); // Hellion Chat v0.6.1 — render the one-time hint banner first so it - // sits above the tab area / sidebar in full window width. Stash the - // height for GetRemainingHeightForMessageLog so the message log - // shrinks accordingly while the banner is visible. - _v061HintBannerHeight = DrawV061HintBannerIfNeeded(); + // sits above the tab area / sidebar in full window width. ImGui's + // GetContentRegionAvail subtracts its height automatically because the + // cursor advances past it before the message log calls + // GetRemainingHeightForMessageLog, so we don't track the height here. + DrawV061HintBannerIfNeeded(); if (Plugin.Config.SidebarTabView) DrawTabSidebar(); @@ -1540,11 +1568,14 @@ public sealed class ChatLogWindow : Window var startY = ImGui.GetCursorPosY(); var bg = new System.Numerics.Vector4(0.16f, 0.20f, 0.28f, 1f); - ImGui.PushStyleColor(ImGuiCol.ChildBg, bg); - ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1f); - var dismiss = false; var openSettings = false; + // RAII for the style stack so an early return in this block + // (or a later refactor that introduces one) can never leave the + // ImGui style stack unbalanced. Matches the convention used + // elsewhere in this file. + using (ImRaii.PushColor(ImGuiCol.ChildBg, bg)) + using (ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 1f)) using (var child = ImRaii.Child("##v061-pop-out-header-hint", new System.Numerics.Vector2(0f, 84f), true)) { if (child) @@ -1561,8 +1592,6 @@ public sealed class ChatLogWindow : Window } } - ImGui.PopStyleVar(); - ImGui.PopStyleColor(); ImGui.Spacing(); if (dismiss) @@ -1636,13 +1665,6 @@ public sealed class ChatLogWindow : Window internal readonly List PopOutDocked = []; internal readonly HashSet PopOutWindows = []; - // Hellion Chat v0.6.1 — height the v0.6.1 hint banner consumed in the - // current frame, read by GetRemainingHeightForMessageLog so the message - // log can shrink. Unconditionally reassigned at the top of DrawChatLog - // (before any tab-area render) so the value is always in sync with the - // current frame. Returns 0 once the banner is dismissed. - private float _v061HintBannerHeight; - // v0.6.0 — live enumeration of all active Popout windows so the // KeybindManager can find a focused ChatInputBar to forward tab-cycle // keybinds to. Filter on IsOpen prevents touching closed-but-still- @@ -1745,47 +1767,55 @@ public sealed class ChatLogWindow : Window return; var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper()); - - clipper.Begin(AutoCompleteList.Count); - while (clipper.Step()) + try { - for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) + clipper.Begin(AutoCompleteList.Count); + while (clipper.Step()) { - var entry = AutoCompleteList[i]; - - var highlight = AutoCompleteSelection == i; - var clicked = ImGui.Selectable($"{entry.Text}##{entry.Group}/{entry.Row}", highlight) || selected == i; - if (i < 10) + for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) { - var button = (i + 1) % 10; - var text = string.Format(Language.AutoTranslate_Completion_Key, button); - var size = ImGui.CalcTextSize(text); + var entry = AutoCompleteList[i]; - ImGui.SameLine(ImGui.GetContentRegionAvail().X - size.X); + var highlight = AutoCompleteSelection == i; + var clicked = ImGui.Selectable($"{entry.Text}##{entry.Group}/{entry.Row}", highlight) || selected == i; + if (i < 10) + { + var button = (i + 1) % 10; + var text = string.Format(Language.AutoTranslate_Completion_Key, button); + var size = ImGui.CalcTextSize(text); - using (ImRaii.PushColor(ImGuiCol.Text, ImGui.GetStyle().Colors[(int)ImGuiCol.TextDisabled])) - ImGui.TextUnformatted(text); + ImGui.SameLine(ImGui.GetContentRegionAvail().X - size.X); + + using (ImRaii.PushColor(ImGuiCol.Text, ImGui.GetStyle().Colors[(int)ImGuiCol.TextDisabled])) + ImGui.TextUnformatted(text); + } + + if (!clicked) + continue; + + var before = Chat[..AutoCompleteInfo.StartPos]; + var after = Chat[AutoCompleteInfo.EndPos..]; + var replacement = $""; + Chat = $"{before}{replacement}{after}"; + ImGui.CloseCurrentPopup(); + Activate = true; + ActivatePos = AutoCompleteInfo.StartPos + replacement.Length; } - - if (!clicked) - continue; - - var before = Chat[..AutoCompleteInfo.StartPos]; - var after = Chat[AutoCompleteInfo.EndPos..]; - var replacement = $""; - Chat = $"{before}{replacement}{after}"; - ImGui.CloseCurrentPopup(); - Activate = true; - ActivatePos = AutoCompleteInfo.StartPos + replacement.Length; } + + if (!AutoCompleteShouldScroll) + return; + + AutoCompleteShouldScroll = false; + var selectedPos = clipper.StartPosY + clipper.ItemsHeight * (AutoCompleteSelection * 1f); + ImGui.SetScrollFromPosY(selectedPos - ImGui.GetWindowPos().Y); + } + finally + { + // ImGuiListClipperPtr wraps an unmanaged ImGuiListClipper allocated above. + // Without Destroy() the unmanaged block leaks per autocomplete render. + clipper.Destroy(); } - - if (!AutoCompleteShouldScroll) - return; - - AutoCompleteShouldScroll = false; - var selectedPos = clipper.StartPosY + clipper.ItemsHeight * (AutoCompleteSelection * 1f); - ImGui.SetScrollFromPosY(selectedPos - ImGui.GetWindowPos().Y); } private int AutoCompleteCallback(scoped ref ImGuiInputTextCallbackData data) diff --git a/HellionChat/Ui/CommandHelpWindow.cs b/HellionChat/Ui/CommandHelpWindow.cs index 00e6fb8..d0f1b84 100644 --- a/HellionChat/Ui/CommandHelpWindow.cs +++ b/HellionChat/Ui/CommandHelpWindow.cs @@ -47,8 +47,11 @@ public class CommandHelpWindow : Window { Position = pos; SizeConstraints = new WindowSizeConstraints { - MinimumSize = new Vector2(width, 0), - MaximumSize = LogWindow.LastWindowSize with { X = width } + // Use scaledWidth here so the size constraints stay in the same + // coordinate space as Position above; otherwise the help window + // ends up the wrong width at non-100% DPI. + MinimumSize = new Vector2(scaledWidth, 0), + MaximumSize = LogWindow.LastWindowSize with { X = scaledWidth } }; IsOpen = true; diff --git a/HellionChat/Ui/InputPreview.cs b/HellionChat/Ui/InputPreview.cs index 8e7589b..1a95e4c 100644 --- a/HellionChat/Ui/InputPreview.cs +++ b/HellionChat/Ui/InputPreview.cs @@ -177,7 +177,10 @@ public partial class InputPreview : Window return; NextChunkIsAutoTranslate = true; - var payload = (AutoTranslatePayload) chunk.Link!; + // Malformed chunks could carry an AutoTranslateBegin icon without the matching + // payload; bail out instead of dereferencing a null Link. + if (chunk.Link is not AutoTranslatePayload payload) + return; CursorPosition += $"".Length; return; diff --git a/HellionChat/Ui/Popout.cs b/HellionChat/Ui/Popout.cs index 01f3c5d..a3944ff 100644 --- a/HellionChat/Ui/Popout.cs +++ b/HellionChat/Ui/Popout.cs @@ -65,10 +65,22 @@ internal class Popout : Window return FrameTime - lastActivityTime <= 1000 * Plugin.Config.InactivityHideTimeout; } + // Tracks the style instance pushed in PreDraw so PostDraw pops the same + // one even if config changes mid-frame. See AUDIT-2026-05-05 [CR-UI-5]. + private StyleModel? _pushedStyle; + public override void PreDraw() { + _pushedStyle = null; if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null }) - StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == Plugin.Config.ChosenStyle)?.Push(); + { + var style = StyleModel.GetConfiguredStyles()?.FirstOrDefault(s => s.Name == Plugin.Config.ChosenStyle); + if (style != null) + { + style.Push(); + _pushedStyle = style; + } + } Flags = ImGuiWindowFlags.None; if (!Plugin.Config.ShowPopOutTitleBar) @@ -201,8 +213,11 @@ internal class Popout : Window if (Idx >= 0 && Idx < ChatLogWindow.PopOutDocked.Count) ChatLogWindow.PopOutDocked[Idx] = ImGui.IsWindowDocked(); - if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null }) - StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == Plugin.Config.ChosenStyle)?.Pop(); + if (_pushedStyle != null) + { + _pushedStyle.Pop(); + _pushedStyle = null; + } } public override void OnClose() diff --git a/HellionChat/Ui/SeStringDebugger.cs b/HellionChat/Ui/SeStringDebugger.cs index 36d2d1d..465d016 100644 --- a/HellionChat/Ui/SeStringDebugger.cs +++ b/HellionChat/Ui/SeStringDebugger.cs @@ -222,7 +222,16 @@ public class SeStringDebugger : Window default: var payloadData = payload.Encode(); - var initialByte = payloadData.First(); + if (payloadData.Length == 0) + { + RenderMetadataDictionary("Empty Payload", new Dictionary + { + { "Type", payload.GetType().Name }, + }); + break; + } + + var initialByte = payloadData[0]; if (initialByte != 0x02) { RenderMetadataDictionary("Text Payload", new Dictionary diff --git a/HellionChat/Ui/SettingsTabs/Chat.cs b/HellionChat/Ui/SettingsTabs/Chat.cs index be246b0..bb43339 100644 --- a/HellionChat/Ui/SettingsTabs/Chat.cs +++ b/HellionChat/Ui/SettingsTabs/Chat.cs @@ -21,6 +21,10 @@ internal sealed class Chat : ISettingsTab public string Name => HellionStrings.Settings_Tab_Chat + "###tabs-chat"; private SearchSelector.SelectorPopupOptions WordPopupOptions; + // Snapshot of EmoteCache.State for which we last built WordPopupOptions. + // Without this, an empty FilteredSheet (e.g., the user blocked every emote) + // would trigger a refill every frame the settings tab is open. + private EmoteCache.LoadingState? WordPopupOptionsBuiltFor; internal Chat(Plugin plugin, Configuration mutable) { @@ -28,6 +32,7 @@ internal sealed class Chat : ISettingsTab Mutable = mutable; WordPopupOptions = RefillSheet(); + WordPopupOptionsBuiltFor = EmoteCache.State; } private SearchSelector.SelectorPopupOptions RefillSheet() @@ -160,9 +165,12 @@ internal sealed class Chat : ISettingsTab ImGui.TextUnformatted(Language.Options_Emote_BlockedEmotes); ImGui.Spacing(); - if (EmoteCache.State is EmoteCache.LoadingState.Done && WordPopupOptions.FilteredSheet.Length == 0) + if (EmoteCache.State is EmoteCache.LoadingState.Done + && WordPopupOptions.FilteredSheet.Length == 0 + && WordPopupOptionsBuiltFor != EmoteCache.LoadingState.Done) { WordPopupOptions = RefillSheet(); + WordPopupOptionsBuiltFor = EmoteCache.LoadingState.Done; } var buttonWidth = ImGui.GetContentRegionAvail().X / 3; diff --git a/HellionChat/Ui/SettingsTabs/Database.cs b/HellionChat/Ui/SettingsTabs/Database.cs index 0295456..fa93818 100755 --- a/HellionChat/Ui/SettingsTabs/Database.cs +++ b/HellionChat/Ui/SettingsTabs/Database.cs @@ -81,9 +81,11 @@ internal sealed class Database : ISettingsTab { try { + // Delete both legacy files in one click — the previous if/else + // left the second file behind when both happened to exist. if (old.Exists) old.Delete(); - else + if (migratedOld.Exists) migratedOld.Delete(); WrapperUtil.AddNotification(Language.Options_Database_Old_Delete_Success, NotificationType.Success); } diff --git a/HellionChat/Ui/SettingsTabs/Privacy.cs b/HellionChat/Ui/SettingsTabs/Privacy.cs index 4a0f2c0..071155e 100644 --- a/HellionChat/Ui/SettingsTabs/Privacy.cs +++ b/HellionChat/Ui/SettingsTabs/Privacy.cs @@ -615,7 +615,7 @@ internal sealed class Privacy : ISettingsTab CleanupRunning = true; var allowed = Plugin.Config.PrivacyPersistChannels.Select(t => (int)(ushort)t).ToList(); - new Thread(() => + var thread = new Thread(() => { try { @@ -625,10 +625,14 @@ internal sealed class Privacy : ISettingsTab // Bound the wait so a hung framework tick can't deadlock // the background cleanup worker. See the matching comment in // the retention path above for rationale. + // Note: FilterAllTabs() is called synchronously instead of + // FilterAllTabsAsync() — the async variant fires-and-forgets + // a Task.Run, so the .Wait() would return before the filter + // pass actually finishes. See AUDIT-2026-05-05 [QUAL-02]. if (!Plugin.Framework.Run(() => { Plugin.MessageManager.ClearAllTabs(); - Plugin.MessageManager.FilterAllTabsAsync(); + Plugin.MessageManager.FilterAllTabs(); }).Wait(TimeSpan.FromSeconds(5))) { Plugin.Log.Warning("Privacy cleanup: framework refresh timed out after 5s."); @@ -646,6 +650,9 @@ internal sealed class Privacy : ISettingsTab CleanupRunning = false; CleanupCounts = null; } - }).Start(); + }); + // Background thread so a still-running cleanup doesn't hold up FFXIV exit. + thread.IsBackground = true; + thread.Start(); } } diff --git a/HellionChat/Util/AutoTranslate.cs b/HellionChat/Util/AutoTranslate.cs index d3532d1..0728cb6 100644 --- a/HellionChat/Util/AutoTranslate.cs +++ b/HellionChat/Util/AutoTranslate.cs @@ -1,6 +1,5 @@ using System.Diagnostics; using System.Globalization; -using System.Runtime.InteropServices; using System.Text; using Dalamud.Game; using Dalamud.Utility; @@ -233,9 +232,6 @@ internal static class AutoTranslate .ToList(); } - [DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)] - private static extern int memcmp(byte[] b1, byte[] b2, nuint count); - internal static void ReplaceWithPayload(ref byte[] bytes) { var search = " entries.Length) + break; // cycle guard + iconId = entry.Redirect; + continue; + } + return !entry.IsEmpty; } @@ -146,12 +158,17 @@ public readonly unsafe ref struct GfdFileView internal static class IconUtil { private static byte[]? GfdFile; - public static unsafe GfdFileView GfdFileView + public static GfdFileView GfdFileView { get { - GfdFile ??= Plugin.DataManager.GetFile("common/font/gfdata.gfd")!.Data; - return new GfdFileView(new ReadOnlySpan(Unsafe.AsPointer(ref GfdFile[0]), GfdFile.Length)); + if (GfdFile is null) + { + var file = Plugin.DataManager.GetFile("common/font/gfdata.gfd") + ?? throw new FileNotFoundException("Failed to load common/font/gfdata.gfd from the game data."); + GfdFile = file.Data; + } + return new GfdFileView(GfdFile); } } diff --git a/HellionChat/Util/Lender.cs b/HellionChat/Util/Lender.cs index 29132d7..2d49bcd 100755 --- a/HellionChat/Util/Lender.cs +++ b/HellionChat/Util/Lender.cs @@ -8,7 +8,7 @@ internal class Lender internal Lender(Func ctor) { - Ctor = ctor; + Ctor = ctor ?? throw new ArgumentNullException(nameof(ctor)); } internal void ResetCounter() diff --git a/HellionChat/Util/MemoryUtil.cs b/HellionChat/Util/MemoryUtil.cs index b65bab9..417496d 100644 --- a/HellionChat/Util/MemoryUtil.cs +++ b/HellionChat/Util/MemoryUtil.cs @@ -4,8 +4,21 @@ namespace HellionChat.Util; public static class MemoryUtil { + // Diagnostic helper. Pointer dereferences here would crash on a null/garbage + // address and a huge length would log megabytes of raw bytes; both are easy + // to trigger from a debugger and pollute the log with potentially sensitive + // game-state. Validate the inputs before reading. + private const int MaxDumpLength = 4096; + public static unsafe void PrintMemoryArea(nint address, int length) { + if (address == nint.Zero) + throw new ArgumentException("Memory address cannot be zero.", nameof(address)); + if (length <= 0) + throw new ArgumentOutOfRangeException(nameof(length), length, "Length must be positive."); + if (length > MaxDumpLength) + throw new ArgumentOutOfRangeException(nameof(length), length, $"Length exceeds the {MaxDumpLength}-byte safety cap."); + var ptr = (byte*)address; var str = new StringBuilder("\n"); for(var i = 0; i < length; i++) diff --git a/HellionChat/Util/Payloads.cs b/HellionChat/Util/Payloads.cs index 62223a9..b225121 100755 --- a/HellionChat/Util/Payloads.cs +++ b/HellionChat/Util/Payloads.cs @@ -66,6 +66,8 @@ internal class UriPayload(Uri uri) : Payload public static UriPayload ResolveUri(string rawUri) { ArgumentNullException.ThrowIfNull(rawUri); + if (string.IsNullOrWhiteSpace(rawUri)) + throw new UriFormatException("URI cannot be empty or whitespace."); // Check for an expected scheme '://', if not add 'https://' if (ExpectedSchemes.Any(scheme => rawUri.StartsWith($"{scheme}://"))) diff --git a/HellionChat/Util/StringUtil.cs b/HellionChat/Util/StringUtil.cs index 6072be4..e812e13 100755 --- a/HellionChat/Util/StringUtil.cs +++ b/HellionChat/Util/StringUtil.cs @@ -23,6 +23,8 @@ internal static class StringUtil var bytes = Math.Abs(byteCount); var place = Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024))); var num = Math.Round(bytes / Math.Pow(1024, place), 1); - return (Math.Sign(byteCount) * num).ToString("N0") + suf[place]; + // "0.#" keeps the rounded fractional digit (1.5 GB stays "1.5GB"); "N0" + // would truncate it back to integer. + return (Math.Sign(byteCount) * num).ToString("0.#") + suf[place]; } } diff --git a/PRIVACY.md b/PRIVACY.md index a000250..7df5354 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -12,7 +12,7 @@ because no data ever leaves your machine on the maintainer's infrastructure. Independently of that, the plugin is built so that you can act on your own data the way the GDPR expects. -Last reviewed: 2026-05-03 (HellionChat v0.5.4). +Last reviewed: 2026-05-05 (HellionChat v1.0.3). --- @@ -103,8 +103,17 @@ on your behalf. reaches BetterTTV (unavoidable for any HTTPS request); the request itself contains no identifying user data, no character name, no message text. Only the emote ID being looked up is in the URL path. -- **When it triggers:** Only when an incoming message contains an - emote token that is on the BetterTTV emote list. +- **When it triggers:** + - The emote *list* (global emotes plus the top-1500 community emotes + over fifteen API pages) is fetched from `api.betterttv.net` once + per session at plugin startup, provided the **Show emotes** option + is on. This first list-fetch happens before any chat message has + arrived; BetterTTV's edge therefore sees your IP as soon as the + plugin loads, not only after an emote is mentioned. + - The individual emote *images* on `cdn.betterttv.net` are fetched + on demand, only when an incoming chat message contains a token + matching one of the cached IDs. These are cached locally + (`emoteCache/`) and reused across sessions. - **Cached:** Yes, in `emoteCache/`. A given emote is downloaded once per machine and reused. - **How to opt out:** Turn off the **Show emotes** option in diff --git a/docs/THIRD_PARTY_NOTICES.md b/docs/THIRD_PARTY_NOTICES.md index 825be25..400eb3a 100644 --- a/docs/THIRD_PARTY_NOTICES.md +++ b/docs/THIRD_PARTY_NOTICES.md @@ -4,21 +4,22 @@ HellionChat ships and depends on a number of third-party components. This document lists them, their licences and which of them touch the network. It is the inventory referenced by `PRIVACY.md`. -Last reviewed: 2026-05-03 (HellionChat v0.5.4). +Last reviewed: 2026-05-05 (HellionChat v1.0.3). --- ## Direct NuGet dependencies -Pinned in `HellionChat/HellionChat.csproj`. Versions reflect the v1.0.0 build. +Pinned in `HellionChat/HellionChat.csproj`. Versions reflect the v1.0.3 build. | Package | Version | Licence | Network | Purpose | | --- | --- | --- | --- | --- | | [MessagePack](https://github.com/MessagePack-CSharp/MessagePack-CSharp) | 3.1.4 | MIT | no | Binary serialisation for the SQLite message store. | | [Microsoft.Data.Sqlite](https://learn.microsoft.com/dotnet/standard/data/sqlite/) | 10.0.7 | MIT | no | Local SQLite access for the message database. | | [morelinq](https://github.com/morelinq/MoreLINQ) | 4.4.0 | Apache-2.0 | no | LINQ helper extensions. | -| [Pidgin](https://github.com/benjamin-hodgson/Pidgin) | 3.3.0 | MIT | no | Parser combinator library used for chat-input parsing. | +| [Pidgin](https://github.com/benjamin-hodgson/Pidgin) | 3.5.1 | MIT | no | Parser combinator library used for chat-input parsing. CIString Unicode fix relevant for non-ASCII channel/tab names. | | [SixLabors.ImageSharp](https://github.com/SixLabors/ImageSharp) | 3.1.12 | [Six Labors Split License 1.0](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE) (OSI-approved; free for open-source / non-commercial use, commercial licence required for closed-source commercial use) | no | Image decoding for cached emotes. | +| [SQLitePCLRaw.lib.e_sqlite3](https://github.com/ericsink/SQLitePCL.raw) | 3.50.3 | MIT | no | Native SQLite binary, explicitly pinned to override the transitive default for CVE-2025-6965 (memory corruption from aggregate-term overflow) and CVE-2025-7709. | Six Labors note: HellionChat is an EUPL-1.2-licensed open-source project distributed at no cost. Use of ImageSharp 3.x under the -- 2.52.0 From 8db3eca46c0ef2d7991e8c9b9a84b680e99d2d3d Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 07:37:35 +0200 Subject: [PATCH 017/169] chore(fontmanager): drop unused Lodestone font download MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The FontManager constructor downloaded FFXIV_Lodestone_SSF.ttf from img.finalfantasyxiv.com on first start (or read it from a local cache) into a GameSymFont byte array. Both historical readers of that field are gone: - BuildFonts() used to feed the bytes into AddFontFromMemory; that path was replaced by the Dalamud-provided AddGameSymbol helper. - The upstream webinterface server wrote the bytes through a BinaryWriter to serve them to the Svelte frontend; the entire webinterface was intentionally removed in HellionChat. With no live consumer left, the field, the constructor block, the HttpClient call and the disk cache are all dead code. Removing them: - eliminates the synchronous HTTP request on the plugin-load thread (no more multi-second startup hang on slow networks) - closes the implicit "no timeout, no size guard" exposure on that request - removes one outbound network endpoint (Square Enix Lodestone CDN) from the privacy footprint PRIVACY.md and THIRD_PARTY_NOTICES.md updated to reflect that HellionChat now talks to BetterTTV only (opt-out via setting). Cached TTF files left over from earlier versions stay in pluginConfigs/ HellionChat/ until a user removes them; they are simply no longer read. Build: 0 warnings, 0 errors. No behavioural change for users — symbol glyphs (job icons, item glyphs, status effects) keep rendering through Dalamud's built-in symbol font. --- HellionChat/FontManager.cs | 28 ---------------------- PRIVACY.md | 48 +++++++++++++++++-------------------- docs/THIRD_PARTY_NOTICES.md | 6 +++-- 3 files changed, 26 insertions(+), 56 deletions(-) diff --git a/HellionChat/FontManager.cs b/HellionChat/FontManager.cs index df83ef0..f8bac37 100644 --- a/HellionChat/FontManager.cs +++ b/HellionChat/FontManager.cs @@ -18,8 +18,6 @@ public class FontManager internal IFontHandle FontAwesome = null!; - internal readonly byte[] GameSymFont; - private ushort[] Ranges = []; private ushort[] JpRange = []; @@ -30,32 +28,6 @@ public class FontManager 36f, 40f, 45f, 46f, 68f, 90f, ]; - public FontManager() - { - var filePath = Path.Combine(Plugin.Interface.ConfigDirectory.FullName, "FFXIV_Lodestone_SSF.ttf"); - if (File.Exists(filePath)) - { - GameSymFont = File.ReadAllBytes(filePath); - } - else - { - // Dispose HttpClient and HttpResponseMessage to avoid socket - // exhaustion on repeated cold-start downloads. GetAwaiter().GetResult() - // unwraps AggregateException so failures surface cleanly. A full - // async refactor of the constructor would be cleaner but is out of - // scope for v1.0.0 — tracked in the backlog. - using var client = new HttpClient(); - using var response = client - .GetAsync("https://img.finalfantasyxiv.com/lds/pc/global/fonts/FFXIV_Lodestone_SSF.ttf") - .GetAwaiter() - .GetResult(); - response.EnsureSuccessStatusCode(); - GameSymFont = response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult(); - - Dalamud.Utility.FilesystemUtil.WriteAllBytesSafe(filePath, GameSymFont); - } - } - /// /// Backing bytes for the bundled Hellion font (Exo 2, OFL-1.1). Lazily /// extracted from the assembly's manifest resources on first use; the diff --git a/PRIVACY.md b/PRIVACY.md index 7df5354..e8c2e7a 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -23,10 +23,9 @@ Last reviewed: 2026-05-05 (HellionChat v1.0.3). - The plugin does not phone home. There is no telemetry, no analytics, no crash reporter, no usage counter, no remote update check beyond what Dalamud itself does. -- Two outbound network calls exist by design: the BetterTTV emote - service (for chat emotes) and the Square Enix Lodestone font CDN - (for the in-game symbol font). Both are documented in detail below - and both can be reasoned about per request. +- One outbound network call exists by design: the BetterTTV emote + service (for chat emotes). It is documented in detail below and + can be reasoned about per request. - You can export every message the plugin has stored, in Markdown, JSON or CSV, and you can wipe stored history per channel, per date range, or globally. @@ -123,24 +122,22 @@ on your behalf. Source: `HellionChat/EmoteCache.cs`. -### 2. Square Enix Lodestone font (`img.finalfantasyxiv.com`) +### 2. Square Enix Lodestone font — removed in v1.0.4 -- **What it does:** Downloads the `FFXIV_Lodestone_SSF.ttf` font file - from the official Square Enix Lodestone CDN once during font setup, - so the plugin can render in-game special symbols (job icons, item - glyphs, etc.) inside ImGui. -- **What is sent:** A single HTTPS GET request to the public Square - Enix font URL. Your IP address reaches Square Enix (unavoidable); - no character data, no plugin identifier, no message content. -- **When it triggers:** Once per font initialisation, not per session - if the file is already cached locally. -- **Cached:** Yes, by Dalamud's font subsystem. -- **How to opt out:** This call is part of the font pipeline inherited - from upstream Chat 2 and not toggleable from the settings UI today. - If a user-facing opt-out for this would be useful for you, please - open a feature-request issue. +Earlier versions of HellionChat (and upstream Chat 2) downloaded +`FFXIV_Lodestone_SSF.ttf` from `img.finalfantasyxiv.com` once during +font setup. That code path was a leftover from upstream's removed +webinterface feature and was no longer consumed anywhere — the in-game +symbol glyphs (job icons, item glyphs, status effects) come from +Dalamud's bundled symbol-font helper, not from the downloaded TTF. -Source: `HellionChat/FontManager.cs`. +The download was removed in v1.0.4. As of that version HellionChat +makes no automatic network call to Square Enix or to any +`finalfantasyxiv.com` host. + +Cached `FFXIV_Lodestone_SSF.ttf` files left over from earlier versions +remain in `pluginConfigs/HellionChat/` until manually deleted; they +are no longer read. ### Links you click yourself (no automatic traffic) @@ -218,14 +215,13 @@ retroactive cleanup to apply retroactively, by design. | Party | Why they appear | What reaches them | Their privacy policy | | --- | --- | --- | --- | | BetterTTV (NightDev LLC) | Optional emote rendering | HTTPS request for an emote ID; your IP | | -| Square Enix | Lodestone font download (once) | HTTPS request for the font file; your IP | | | GitHub (Microsoft) | Plugin distribution via custom repo, issue tracker | Whatever GitHub sees from any HTTPS request to a public repo | | | Dalamud / XIVLauncher (goatcorp) | Plugin loader, font subsystem, repo polling | Whatever Dalamud reports for itself; out of HellionChat's scope | | -Square Enix and GitHub are unavoidable for anyone playing FFXIV -through Dalamud at all. BetterTTV is the only third party HellionChat -introduces on top of the baseline that is not also part of using FFXIV -or Dalamud, and BetterTTV is opt-out via settings. +GitHub and the Dalamud/XIVLauncher loader are unavoidable for anyone +playing FFXIV through Dalamud at all. BetterTTV is the only third +party HellionChat introduces on top of that baseline, and it is +opt-out via settings. --- @@ -241,7 +237,7 @@ direct dependencies the plugin pulls in: - `SixLabors.ImageSharp` — image decoding (used for the BetterTTV emote pipeline), no network on its own. -The two network calls listed under "Outbound network calls" are +The single network call listed under "Outbound network calls" is written directly in HellionChat's own source, not delegated to a dependency. diff --git a/docs/THIRD_PARTY_NOTICES.md b/docs/THIRD_PARTY_NOTICES.md index 400eb3a..81790bb 100644 --- a/docs/THIRD_PARTY_NOTICES.md +++ b/docs/THIRD_PARTY_NOTICES.md @@ -64,8 +64,10 @@ traffic is initiated explicitly by HellionChat's own source files and is documented in `PRIVACY.md` under "Outbound network calls": - `HellionChat/EmoteCache.cs` → BetterTTV API + CDN (opt-out via setting) -- `HellionChat/FontManager.cs` → Square Enix Lodestone font CDN (one-time - download) + +The earlier Square Enix Lodestone font download (`FontManager.cs`) +was removed in v1.0.4 — it was a leftover from upstream's removed +webinterface feature and was no longer consumed. --- -- 2.52.0 From 08b2ffc600ce9eb1f80177ceefb959e4232248f9 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 07:45:37 +0200 Subject: [PATCH 018/169] ci(codeql): pin actions to commit SHAs Replaces floating major-version tags with full commit SHAs (Tag- Kommentar dahinter), so a tag-republish can't slip a different action into the workflow. --- .github/workflows/codeql.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 9707aaa..2b78608 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -39,10 +39,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup .NET 10 - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 with: dotnet-version: 10.0.x @@ -55,7 +55,7 @@ jobs: Expand-Archive -Force -Path dalamud.zip -DestinationPath $hooks - name: Initialize CodeQL - uses: github/codeql-action/init@v4 + uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 with: languages: csharp build-mode: manual @@ -68,7 +68,7 @@ jobs: run: dotnet build HellionChat/HellionChat.csproj --configuration Release --no-restore - name: Perform CodeQL analysis - uses: github/codeql-action/analyze@v4 + uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 with: category: /language:csharp @@ -79,15 +79,15 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Initialize CodeQL - uses: github/codeql-action/init@v4 + uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 with: languages: actions build-mode: none - name: Perform CodeQL analysis - uses: github/codeql-action/analyze@v4 + uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 with: category: /language:actions -- 2.52.0 From 497197eb2cf8a63729e997ba606449d3ba206b7f Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 07:54:33 +0200 Subject: [PATCH 019/169] chore(deps): cap major-bump packages with closed version ranges ImageSharp, MessagePack and Pidgin pinned to [x.y, next-major) so a lock-file regeneration cannot drift across a major. Resolved versions unchanged; lock-file diff is request-string only. --- HellionChat/HellionChat.csproj | 10 +++++++--- HellionChat/packages.lock.json | 6 +++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/HellionChat/HellionChat.csproj b/HellionChat/HellionChat.csproj index d56f76c..9ba2e94 100644 --- a/HellionChat/HellionChat.csproj +++ b/HellionChat/HellionChat.csproj @@ -18,7 +18,11 @@ - + + - - + + diff --git a/HellionChat/packages.lock.json b/HellionChat/packages.lock.json index 8f6f61c..d3d3517 100644 --- a/HellionChat/packages.lock.json +++ b/HellionChat/packages.lock.json @@ -16,7 +16,7 @@ }, "MessagePack": { "type": "Direct", - "requested": "[3.1.4, )", + "requested": "[3.1.4, 4.0.0)", "resolved": "3.1.4", "contentHash": "BH0wlHWmVoZpbAPyyt2Awbq30C+ZsS3eHSkYdnyUAbqVJ22fAJDzn2xTieBeoT5QlcBzp61vHcv878YJGfi3mg==", "dependencies": { @@ -44,13 +44,13 @@ }, "Pidgin": { "type": "Direct", - "requested": "[3.5.1, )", + "requested": "[3.5.1, 4.0.0)", "resolved": "3.5.1", "contentHash": "zU7tkXlF3D6d2GLTjJDomAL3nnl4AwfZvSgSz8D4b+Ry21/clqedYlxBnEAkAU/bkGfEv6uRR7QCdWZUpKrB/g==" }, "SixLabors.ImageSharp": { "type": "Direct", - "requested": "[3.1.12, )", + "requested": "[3.1.12, 4.0.0)", "resolved": "3.1.12", "contentHash": "iAg6zifihXEFS/t7fiHhZBGAdCp3FavsF4i2ZIDp0JfeYeDVzvmlbY1CNhhIKimaIzrzSi5M/NBFcWvZT2rB/A==" }, -- 2.52.0 From e7c8667497d87a781b0deeb243271b033b3799a2 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 08:09:53 +0200 Subject: [PATCH 020/169] fix(emotecache): cancel pending texture loads on plugin dispose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plugin-scoped CancellationTokenSource fließt jetzt durch LoadAsync und die Texture-Calls; Dispose cancelt in-flight downloads. Smoke (System- Spam + Reload) sauber, weiter beobachten unter höherem Emote-Volumen. --- HellionChat/EmoteCache.cs | 75 ++++++++++++++++++++++++++++++--------- 1 file changed, 58 insertions(+), 17 deletions(-) diff --git a/HellionChat/EmoteCache.cs b/HellionChat/EmoteCache.cs index f2c818c..97d5dc8 100644 --- a/HellionChat/EmoteCache.cs +++ b/HellionChat/EmoteCache.cs @@ -66,16 +66,29 @@ public static class EmoteCache public static string[] SortedCodeArray = []; + // Plugin-scoped cancellation source for in-flight emote loads. Dispose + // cancels every running download/texture-create so the workers don't + // touch a torn-down TextureProvider on plugin reload. Replaced with a + // fresh source on the next LoadData() call so a re-enable still works. + private static CancellationTokenSource Cts = new(); + internal static CancellationToken Token => Cts.Token; + public static async Task LoadData() { if (State is not LoadingState.Unloaded) return; + // Refresh the CTS in case Dispose was called and we're being re-enabled + // in the same process (Dalamud /xlplugins toggle). + if (Cts.IsCancellationRequested) + Cts = new CancellationTokenSource(); + State = LoadingState.Loading; + var ct = Cts.Token; try { - var global = await Client.GetAsync(GlobalEmotes); - var globalList = await global.Content.ReadAsStringAsync(); + var global = await Client.GetAsync(GlobalEmotes, ct); + var globalList = await global.Content.ReadAsStringAsync(ct); foreach (var emote in JsonSerializer.Deserialize(globalList)!) if (!string.IsNullOrEmpty(emote.Code) && !NotWorking.Contains(emote.Code)) @@ -84,8 +97,8 @@ public static class EmoteCache var lastId = string.Empty; for (var i = 0; i < 15; i++) { - var top = await Client.GetAsync(Top100Emotes.Format(BetterTTV, lastId)); - var topList = await top.Content.ReadAsStringAsync(); + var top = await Client.GetAsync(Top100Emotes.Format(BetterTTV, lastId), ct); + var topList = await top.Content.ReadAsStringAsync(ct); var jsonList = JsonSerializer.Deserialize>(topList)!; // BetterTTV occasionally returns entries with a null Code; the @@ -103,6 +116,12 @@ public static class EmoteCache SortedCodeArray = Cache.Keys.Order().ToArray(); State = LoadingState.Done; } + catch (OperationCanceledException) + { + // Plugin disposed while the cache was loading; leave State on + // Loading so a subsequent re-enable can re-issue LoadData with + // a fresh CTS (handled above). + } catch (Exception ex) { // Reset to Unloaded so a later trigger (e.g. the user reopening @@ -116,6 +135,10 @@ public static class EmoteCache public static void Dispose() { + // Cancel in-flight downloads / texture creates so the async-void + // Load methods bail out before they touch a disposed TextureProvider. + Cts.Cancel(); + foreach (var emote in EmoteImages.Values) emote.InnerDispose(); } @@ -171,7 +194,7 @@ public static class EmoteCache ImGui.Image(Texture!.Handle, size); } - internal async Task LoadAsync(Emote emote) + internal async Task LoadAsync(Emote emote, CancellationToken ct) { // BetterTTV-supplied Id and ImageType are interpolated straight // into the filename. HTTPS protects the wire, but a compromised @@ -188,15 +211,15 @@ public static class EmoteCache if (File.Exists(filePath)) { - RawData = await File.ReadAllBytesAsync(filePath); + RawData = await File.ReadAllBytesAsync(filePath, ct); } else { - var content = await Client.GetAsync(EmotePath.Format(emote.Id)); - RawData = await content.Content.ReadAsByteArrayAsync(); + var content = await Client.GetAsync(EmotePath.Format(emote.Id), ct); + RawData = await content.Content.ReadAsByteArrayAsync(ct); await using var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read); - stream.Write(RawData, 0, RawData.Length); + await stream.WriteAsync(RawData, ct); } return RawData; @@ -209,21 +232,28 @@ public static class EmoteCache { public ImGuiEmote Prepare(Emote emote) { - Task.Run(() => Load(emote)); + var ct = EmoteCache.Token; + Task.Run(() => Load(emote, ct), ct); return this; } - private async void Load(Emote emote) + private async void Load(Emote emote, CancellationToken ct) { try { - var image = await LoadAsync(emote); + var image = await LoadAsync(emote, ct); if (image.Length <= 0) return; - Texture = await Plugin.TextureProvider.CreateFromImageAsync(image); + ct.ThrowIfCancellationRequested(); + Texture = await Plugin.TextureProvider.CreateFromImageAsync(image, cancellationToken: ct); IsLoaded = true; } + catch (OperationCanceledException) + { + // Plugin disposed mid-load; the EmoteImages entry is also + // being torn down, no extra cleanup needed. + } catch (Exception ex) { Failed = true; @@ -279,15 +309,16 @@ public static class EmoteCache public ImGuiGif Prepare(Emote emote) { - Task.Run(() => Load(emote)); + var ct = EmoteCache.Token; + Task.Run(() => Load(emote, ct), ct); return this; } - private async void Load(Emote emote) + private async void Load(Emote emote, CancellationToken ct) { try { - var image = await LoadAsync(emote); + var image = await LoadAsync(emote, ct); if (image.Length <= 0) return; @@ -299,6 +330,8 @@ public static class EmoteCache var frames = new List<(IDalamudTextureWrap Tex, float Delay)>(); foreach (var frame in img.Frames) { + ct.ThrowIfCancellationRequested(); + var delay = frame.Metadata.GetGifMetadata().FrameDelay / 100f; // Follows the same pattern as browsers, anything under 0.02s delay will be rounded up to 0.1s @@ -307,13 +340,21 @@ public static class EmoteCache var buffer = new byte[4 * frame.Width * frame.Height]; frame.CopyPixelDataTo(buffer); - var tex = await Plugin.TextureProvider.CreateFromRawAsync(RawImageSpecification.Rgba32(frame.Width, frame.Height), buffer); + var tex = await Plugin.TextureProvider.CreateFromRawAsync(RawImageSpecification.Rgba32(frame.Width, frame.Height), buffer, cancellationToken: ct); frames.Add((tex, delay)); } Frames = frames; IsLoaded = true; } + catch (OperationCanceledException) + { + // Plugin disposed mid-load; partial frames are released by + // InnerDispose on the next dispose pass. + foreach (var f in Frames) + f.Texture.Dispose(); + Frames = []; + } catch (Exception ex) { Failed = true; -- 2.52.0 From f093d93761d041b46af2d15f3c82f54698359c6c Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 08:23:54 +0200 Subject: [PATCH 021/169] perf(messagemanager): switch pending queue to linked list, quiet privacy log MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PendingSync läuft jetzt als LinkedList (O(1) Last statt O(n) Linq-Last im ContentIdResolverHook); Privacy-Filter-Drop-Log auf Verbose runter, sodass der Default-xllog-Stream nicht mehr pro Nachricht spammt. --- HellionChat/MessageManager.cs | 20 +++++++++++++------- HellionChat/MessageStore.cs | 5 ++++- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/HellionChat/MessageManager.cs b/HellionChat/MessageManager.cs index a7b60e5..177d343 100644 --- a/HellionChat/MessageManager.cs +++ b/HellionChat/MessageManager.cs @@ -34,7 +34,10 @@ internal class MessageManager : IAsyncDisposable // After that, the message is enqueued in the PendingAsync queue, which will // be consumed in a separate thread and perform more processing (emotes, // URLs) as well as inserting the message into the database. - private Queue PendingSync { get; } = []; + // LinkedList instead of Queue: ContentIdResolver hits PendingSync.Last + // every hook call. Queue.Last() is the LINQ extension and walks the + // whole queue (O(n)); LinkedList.Last is an O(1) node reference. + private LinkedList PendingSync { get; } = []; private ConcurrentQueue PendingAsync { get; } = []; private readonly Thread PendingMessageThread; private readonly CancellationTokenSource PendingThreadCancellationToken = new(); @@ -117,8 +120,11 @@ internal class MessageManager : IAsyncDisposable LastContentId = contentId; // Drain the PendingSync queue into the PendingAsync queue. - while (PendingSync.TryDequeue(out var pending)) - PendingAsync.Enqueue(pending); + while (PendingSync.First is { } first) + { + PendingSync.RemoveFirst(); + PendingAsync.Enqueue(first.Value); + } } private void ProcessPendingMessages(CancellationToken token) @@ -223,7 +229,7 @@ internal class MessageManager : IAsyncDisposable // We delay messages to be handed off to the async processing thread // in the next tick, otherwise we can't get the content ID from the hook // below. - PendingSync.Enqueue(pendingMessage); + PendingSync.AddLast(pendingMessage); } // This hook is called immediately after receiving a message with the @@ -235,11 +241,11 @@ internal class MessageManager : IAsyncDisposable try { ContentIdResolverHook?.Original(agent, contentId, accountId, messageIndex, worldId, chatType); - if (PendingSync.Count == 0) + if (PendingSync.Last is not { } last) return; - PendingSync.Last().ContentId = contentId; - PendingSync.Last().AccountId = accountId; + last.Value.ContentId = contentId; + last.Value.AccountId = accountId; } catch (Exception ex) { diff --git a/HellionChat/MessageStore.cs b/HellionChat/MessageStore.cs index 0517cfc..a257f3a 100644 --- a/HellionChat/MessageStore.cs +++ b/HellionChat/MessageStore.cs @@ -452,7 +452,10 @@ internal class MessageStore : IDisposable // covers any future write paths e.g. webinterface backfill). if (!Plugin.Config.IsAllowedForStorage(message.Code.Type)) { - Plugin.Log.Debug($"Privacy filter dropped message: ChatType={message.Code.Type}"); + // Verbose-only: this fires for every dropped message, which is + // the common case for users with a tight privacy whitelist. Keep + // it for diagnostics but stay out of the default xllog stream. + Plugin.Log.Verbose($"Privacy filter dropped message: ChatType={message.Code.Type}"); return; } -- 2.52.0 From 1c511a147de3c8214827c46ba3318e43ec56f22f Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 08:34:56 +0200 Subject: [PATCH 022/169] fix(stringutil): use InvariantCulture for byte-size formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Locale-Bug: BytesToString rendert auf deutscher Locale "1,5GB" statt "1.5GB". InvariantCulture pinnt den Dezimal-Separator. Plus InternalsVisibleTo-Hook für ein lokales (gitignored) Test-Projekt. --- .gitignore | 4 ++++ HellionChat.sln | 17 ++++++++++++++++- HellionChat/HellionChat.csproj | 7 +++++++ HellionChat/Util/StringUtil.cs | 6 ++++-- 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index c9eb026..26fb2a6 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,10 @@ .vscode/ scripts/ +# Local test project (stays out of the published plugin repo; +# pure-function safety net for refactor cycles) +HellionChat.Tests/ + # Packaging pack/ diff --git a/HellionChat.sln b/HellionChat.sln index fa2740a..d0908f0 100755 --- a/HellionChat.sln +++ b/HellionChat.sln @@ -1,16 +1,31 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HellionChat", "HellionChat\HellionChat.csproj", "{739F75E6-B65F-41EF-9D90-F7BC519E4875}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|Any CPU.Build.0 = Debug|Any CPU + {739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|x64.ActiveCfg = Debug|Any CPU + {739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|x64.Build.0 = Debug|Any CPU + {739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|x86.ActiveCfg = Debug|Any CPU + {739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|x86.Build.0 = Debug|Any CPU {739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|Any CPU.ActiveCfg = Release|Any CPU {739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|Any CPU.Build.0 = Release|Any CPU + {739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|x64.ActiveCfg = Release|Any CPU + {739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|x64.Build.0 = Release|Any CPU + {739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|x86.ActiveCfg = Release|Any CPU + {739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection EndGlobal diff --git a/HellionChat/HellionChat.csproj b/HellionChat/HellionChat.csproj index 9ba2e94..9e045f3 100644 --- a/HellionChat/HellionChat.csproj +++ b/HellionChat/HellionChat.csproj @@ -36,6 +36,13 @@ + + + + + True diff --git a/HellionChat/Util/StringUtil.cs b/HellionChat/Util/StringUtil.cs index e812e13..fdddc0a 100755 --- a/HellionChat/Util/StringUtil.cs +++ b/HellionChat/Util/StringUtil.cs @@ -1,3 +1,4 @@ +using System.Globalization; using System.Text; namespace HellionChat.Util; @@ -24,7 +25,8 @@ internal static class StringUtil var place = Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024))); var num = Math.Round(bytes / Math.Pow(1024, place), 1); // "0.#" keeps the rounded fractional digit (1.5 GB stays "1.5GB"); "N0" - // would truncate it back to integer. - return (Math.Sign(byteCount) * num).ToString("0.#") + suf[place]; + // would truncate it back to integer. InvariantCulture pins the decimal + // separator to '.' so a German locale doesn't render "1,5GB". + return (Math.Sign(byteCount) * num).ToString("0.#", CultureInfo.InvariantCulture) + suf[place]; } } -- 2.52.0 From 7e036c1d00948dcc6917cf3eec7432e62e8b3d8d Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 08:48:04 +0200 Subject: [PATCH 023/169] chore(csproj): enable nullable reference types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit-Tooling hatte einen mehrstündigen Sweep mit 50–200 erwarteten Warnings prognostiziert. Tatsächliches Resultat: eine Zeile. Genau eine. Codebase war pro-File längst nullable-konform, wir hatten den Project-Switch nur nie umgelegt. Reminder dass Audit-Output ein Hinweis ist, kein Plan, und ein menschlicher Pass davor lohnt sich. --- HellionChat/HellionChat.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/HellionChat/HellionChat.csproj b/HellionChat/HellionChat.csproj index 9e045f3..0d048e8 100644 --- a/HellionChat/HellionChat.csproj +++ b/HellionChat/HellionChat.csproj @@ -6,6 +6,7 @@ derives from. --> 1.0.3 enable + enable true -- 2.52.0 From db95ec7dffda47edcc9088e59a23765288e70349 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 10:18:49 +0200 Subject: [PATCH 024/169] feat(themes): theme colors record --- HellionChat/Themes/ThemeColors.cs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 HellionChat/Themes/ThemeColors.cs diff --git a/HellionChat/Themes/ThemeColors.cs b/HellionChat/Themes/ThemeColors.cs new file mode 100644 index 0000000..192ff71 --- /dev/null +++ b/HellionChat/Themes/ThemeColors.cs @@ -0,0 +1,31 @@ +namespace HellionChat.Themes; + +// Color-Werte als 0xRRGGBBAA, RgbaToAbgr handled den Byte-Swap zu ImGui. +public sealed record ThemeColors( + uint PrimaryDark, + uint Primary, + uint PrimaryLight, + uint PrimaryGlow, + + uint AccentDark, + uint Accent, + uint AccentLight, + + uint Identity, + + uint WindowBg, + uint ChildBg, + uint FrameBg, + uint Surface, + uint SurfaceHover, + uint Border, + + uint TextPrimary, + uint TextMuted, + uint TextDim, + + uint StatusSuccess, + uint StatusDanger, + uint StatusWarning, + uint StatusInfo +); -- 2.52.0 From 990edd8300580d9c38e73c6c53e1f3448d054b7c Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 10:19:03 +0200 Subject: [PATCH 025/169] feat(themes): theme layout record --- HellionChat/Themes/ThemeLayout.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 HellionChat/Themes/ThemeLayout.cs diff --git a/HellionChat/Themes/ThemeLayout.cs b/HellionChat/Themes/ThemeLayout.cs new file mode 100644 index 0000000..116c2a3 --- /dev/null +++ b/HellionChat/Themes/ThemeLayout.cs @@ -0,0 +1,14 @@ +namespace HellionChat.Themes; + +// Layout-Werte spiegeln die ImGuiStyleVar-Slots, die HellionStyle pusht. +public sealed record ThemeLayout( + float WindowRounding, + float ChildRounding, + float PopupRounding, + float FrameRounding, + float GrabRounding, + float TabRounding, + float ScrollbarRounding, + float WindowBorderSize, + float FrameBorderSize +); -- 2.52.0 From fe9e66b0ff3c53cf52e5e49012a975fb575fc239 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 10:19:18 +0200 Subject: [PATCH 026/169] feat(themes): theme typography record --- HellionChat/Themes/ThemeTypography.cs | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 HellionChat/Themes/ThemeTypography.cs diff --git a/HellionChat/Themes/ThemeTypography.cs b/HellionChat/Themes/ThemeTypography.cs new file mode 100644 index 0000000..b1fbf3a --- /dev/null +++ b/HellionChat/Themes/ThemeTypography.cs @@ -0,0 +1,8 @@ +namespace HellionChat.Themes; + +// Optional pro Theme. v1.1.0 nutzt das nicht aktiv; ist als Erweiterungspunkt +// für zukünftige Theme-Slots vorbereitet. +public sealed record ThemeTypography( + float? OverrideGlobalFontSizePt = null, + float? OverrideSymbolsFontSizePt = null +); -- 2.52.0 From 289fe2eb78bb0fae690ab3e3d3e34e7b4aad9f86 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 10:19:33 +0200 Subject: [PATCH 027/169] feat(themes): theme top-level record --- HellionChat/Themes/Theme.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 HellionChat/Themes/Theme.cs diff --git a/HellionChat/Themes/Theme.cs b/HellionChat/Themes/Theme.cs new file mode 100644 index 0000000..591bf49 --- /dev/null +++ b/HellionChat/Themes/Theme.cs @@ -0,0 +1,12 @@ +namespace HellionChat.Themes; + +public sealed record Theme( + string Slug, + string Name, + string Author, + string Description, + ThemeColors Colors, + ThemeLayout Layout, + ThemeTypography Typography, + bool IsBuiltIn +); -- 2.52.0 From 0b13efd0b5e4c2d6c591955b4b328290e100e148 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 10:21:46 +0200 Subject: [PATCH 028/169] feat(util): add HexToRgba parser for theme JSON --- HellionChat/Util/ColourUtil.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/HellionChat/Util/ColourUtil.cs b/HellionChat/Util/ColourUtil.cs index 63214f0..3373700 100755 --- a/HellionChat/Util/ColourUtil.cs +++ b/HellionChat/Util/ColourUtil.cs @@ -62,4 +62,20 @@ internal static class ColourUtil { return ((uint) a << 24) | ((uint) nb << 16) | ((uint) ng << 8) | nr; } + + public static uint HexToRgba(string hex) + { + ArgumentNullException.ThrowIfNull(hex); + var s = hex.StartsWith('#') ? hex[1..] : hex; + if (s.Length != 6 && s.Length != 8) + throw new FormatException($"Hex colour must be 6 or 8 hex digits, got {s.Length}: '{hex}'"); + + if (!uint.TryParse(s, System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out var value)) + throw new FormatException($"Hex colour '{hex}' is not a valid hexadecimal value"); + + if (s.Length == 6) + value = (value << 8) | 0xFFu; // RRGGBB → RRGGBBFF + + return value; + } } -- 2.52.0 From 48f1fb5ba1ef5f4262ea42b0bf13d135e00031b2 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 10:23:51 +0200 Subject: [PATCH 029/169] feat(themes): hellion-arctic built-in theme --- HellionChat/Themes/Builtin/HellionArctic.cs | 50 +++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 HellionChat/Themes/Builtin/HellionArctic.cs diff --git a/HellionChat/Themes/Builtin/HellionArctic.cs b/HellionChat/Themes/Builtin/HellionArctic.cs new file mode 100644 index 0000000..38ed5b6 --- /dev/null +++ b/HellionChat/Themes/Builtin/HellionArctic.cs @@ -0,0 +1,50 @@ +using HellionChat.Util; + +namespace HellionChat.Themes.Builtin; + +internal static class HellionArctic +{ + public const string Slug = "hellion-arctic"; + + public static Theme Build() => new( + Slug: Slug, + Name: "Hellion Arctic", + Author: "Hellion Online Media", + Description: "Arctic Cyan + Ember Glow on industrial slate. Plugin default.", + Colors: new ThemeColors( + PrimaryDark: ColourUtil.HexToRgba("#0097A7"), + Primary: ColourUtil.HexToRgba("#00BED2"), + PrimaryLight: ColourUtil.HexToRgba("#4DD9E8"), + PrimaryGlow: ColourUtil.HexToRgba("#00BED299"), + + AccentDark: ColourUtil.HexToRgba("#E85D04"), + Accent: ColourUtil.HexToRgba("#F97316"), + AccentLight: ColourUtil.HexToRgba("#FB923C"), + + Identity: ColourUtil.HexToRgba("#0097A7"), + + WindowBg: ColourUtil.HexToRgba("#070B12"), + ChildBg: ColourUtil.HexToRgba("#0C1220"), + FrameBg: ColourUtil.HexToRgba("#141E30"), + Surface: ColourUtil.HexToRgba("#1A2538"), + SurfaceHover: ColourUtil.HexToRgba("#22303F"), + Border: ColourUtil.HexToRgba("#00BED266"), + + TextPrimary: ColourUtil.HexToRgba("#E6F4F1"), + TextMuted: ColourUtil.HexToRgba("#8FA3B5"), + TextDim: ColourUtil.HexToRgba("#566273"), + + StatusSuccess: ColourUtil.HexToRgba("#5CB85C"), + StatusDanger: ColourUtil.HexToRgba("#D9534F"), + StatusWarning: ColourUtil.HexToRgba("#F0AD4E"), + StatusInfo: ColourUtil.HexToRgba("#00BED2") + ), + Layout: new ThemeLayout( + WindowRounding: 4f, ChildRounding: 3f, PopupRounding: 3f, + FrameRounding: 2f, GrabRounding: 2f, TabRounding: 2f, + ScrollbarRounding: 2f, WindowBorderSize: 1f, FrameBorderSize: 1f + ), + Typography: new ThemeTypography(), + IsBuiltIn: true + ); +} -- 2.52.0 From d3d28924e64a383a85e9b351099da68d34540ebd Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 10:24:17 +0200 Subject: [PATCH 030/169] feat(themes): chat2-classic built-in theme --- HellionChat/Themes/Builtin/Chat2Classic.cs | 50 ++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 HellionChat/Themes/Builtin/Chat2Classic.cs diff --git a/HellionChat/Themes/Builtin/Chat2Classic.cs b/HellionChat/Themes/Builtin/Chat2Classic.cs new file mode 100644 index 0000000..f0ba9e8 --- /dev/null +++ b/HellionChat/Themes/Builtin/Chat2Classic.cs @@ -0,0 +1,50 @@ +using HellionChat.Util; + +namespace HellionChat.Themes.Builtin; + +internal static class Chat2Classic +{ + public const string Slug = "chat2-classic"; + + public static Theme Build() => new( + Slug: Slug, + Name: "Chat 2 Klassik", + Author: "Upstream (Infi & Anna)", + Description: "Steel-blue accents on neutral dark grey, eckige Kanten. Vertraut für ChatTwo-Veteranen.", + Colors: new ThemeColors( + PrimaryDark: ColourUtil.HexToRgba("#3D6E92"), + Primary: ColourUtil.HexToRgba("#4682B4"), + PrimaryLight: ColourUtil.HexToRgba("#5C9DC8"), + PrimaryGlow: ColourUtil.HexToRgba("#4682B466"), + + AccentDark: ColourUtil.HexToRgba("#3D6E92"), + Accent: ColourUtil.HexToRgba("#4682B4"), + AccentLight: ColourUtil.HexToRgba("#5C9DC8"), + + Identity: ColourUtil.HexToRgba("#4682B4"), + + WindowBg: ColourUtil.HexToRgba("#0F0F0FF2"), + ChildBg: ColourUtil.HexToRgba("#141414"), + FrameBg: ColourUtil.HexToRgba("#1A1A1A"), + Surface: ColourUtil.HexToRgba("#202020"), + SurfaceHover: ColourUtil.HexToRgba("#2C2C2C"), + Border: ColourUtil.HexToRgba("#404040"), + + TextPrimary: ColourUtil.HexToRgba("#E6E6E6"), + TextMuted: ColourUtil.HexToRgba("#999999"), + TextDim: ColourUtil.HexToRgba("#666666"), + + StatusSuccess: ColourUtil.HexToRgba("#5CB85C"), + StatusDanger: ColourUtil.HexToRgba("#D9534F"), + StatusWarning: ColourUtil.HexToRgba("#F0AD4E"), + StatusInfo: ColourUtil.HexToRgba("#4682B4") + ), + Layout: new ThemeLayout( + WindowRounding: 0f, ChildRounding: 0f, PopupRounding: 0f, + FrameRounding: 0f, GrabRounding: 0f, TabRounding: 0f, + ScrollbarRounding: 0f, WindowBorderSize: 1f, FrameBorderSize: 1f + ), + Typography: new ThemeTypography(), + IsBuiltIn: true + ); +} -- 2.52.0 From 537b96c79f639375894186d49c76cf40dcdf1e95 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 10:24:39 +0200 Subject: [PATCH 031/169] feat(themes): event-horizon built-in theme --- HellionChat/Themes/Builtin/EventHorizon.cs | 50 ++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 HellionChat/Themes/Builtin/EventHorizon.cs diff --git a/HellionChat/Themes/Builtin/EventHorizon.cs b/HellionChat/Themes/Builtin/EventHorizon.cs new file mode 100644 index 0000000..ac3b625 --- /dev/null +++ b/HellionChat/Themes/Builtin/EventHorizon.cs @@ -0,0 +1,50 @@ +using HellionChat.Util; + +namespace HellionChat.Themes.Builtin; + +internal static class EventHorizon +{ + public const string Slug = "event-horizon"; + + public static Theme Build() => new( + Slug: Slug, + Name: "Event Horizon", + Author: "Hellion Online Media", + Description: "Cosmic Purple auf Near-Black. Deep-Space-Stimmung.", + Colors: new ThemeColors( + PrimaryDark: ColourUtil.HexToRgba("#7B3FCF"), + Primary: ColourUtil.HexToRgba("#9D5CFF"), + PrimaryLight: ColourUtil.HexToRgba("#B585FF"), + PrimaryGlow: ColourUtil.HexToRgba("#9D5CFF99"), + + AccentDark: ColourUtil.HexToRgba("#C9982E"), + Accent: ColourUtil.HexToRgba("#E0AB36"), + AccentLight: ColourUtil.HexToRgba("#F2C25C"), + + Identity: ColourUtil.HexToRgba("#9D5CFF"), + + WindowBg: ColourUtil.HexToRgba("#040308"), + ChildBg: ColourUtil.HexToRgba("#0A081A"), + FrameBg: ColourUtil.HexToRgba("#140F23"), + Surface: ColourUtil.HexToRgba("#1B1530"), + SurfaceHover: ColourUtil.HexToRgba("#251D40"), + Border: ColourUtil.HexToRgba("#9D5CFF44"), + + TextPrimary: ColourUtil.HexToRgba("#E6E0F5"), + TextMuted: ColourUtil.HexToRgba("#9890B5"), + TextDim: ColourUtil.HexToRgba("#5A5570"), + + StatusSuccess: ColourUtil.HexToRgba("#26A269"), + StatusDanger: ColourUtil.HexToRgba("#ED333B"), + StatusWarning: ColourUtil.HexToRgba("#E0AB36"), + StatusInfo: ColourUtil.HexToRgba("#9D5CFF") + ), + Layout: new ThemeLayout( + WindowRounding: 6f, ChildRounding: 5f, PopupRounding: 5f, + FrameRounding: 4f, GrabRounding: 4f, TabRounding: 4f, + ScrollbarRounding: 4f, WindowBorderSize: 1f, FrameBorderSize: 1f + ), + Typography: new ThemeTypography(), + IsBuiltIn: true + ); +} -- 2.52.0 From cbfdfe35bedfae01c3831f287f24c1f11487c78d Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 10:25:00 +0200 Subject: [PATCH 032/169] feat(themes): moonlit-bloom built-in theme --- HellionChat/Themes/Builtin/MoonlitBloom.cs | 50 ++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 HellionChat/Themes/Builtin/MoonlitBloom.cs diff --git a/HellionChat/Themes/Builtin/MoonlitBloom.cs b/HellionChat/Themes/Builtin/MoonlitBloom.cs new file mode 100644 index 0000000..07b700d --- /dev/null +++ b/HellionChat/Themes/Builtin/MoonlitBloom.cs @@ -0,0 +1,50 @@ +using HellionChat.Util; + +namespace HellionChat.Themes.Builtin; + +internal static class MoonlitBloom +{ + public const string Slug = "moonlit-bloom"; + + public static Theme Build() => new( + Slug: Slug, + Name: "Moonlit Bloom", + Author: "Hellion Online Media", + Description: "Bloom Magenta + Soft Sage auf Deep Violet Night.", + Colors: new ThemeColors( + PrimaryDark: ColourUtil.HexToRgba("#C957D0"), + Primary: ColourUtil.HexToRgba("#E374E8"), + PrimaryLight: ColourUtil.HexToRgba("#EF8AF4"), + PrimaryGlow: ColourUtil.HexToRgba("#E374E899"), + + AccentDark: ColourUtil.HexToRgba("#7AAC5C"), + Accent: ColourUtil.HexToRgba("#9CCB7C"), + AccentLight: ColourUtil.HexToRgba("#B6E297"), + + Identity: ColourUtil.HexToRgba("#E374E8"), + + WindowBg: ColourUtil.HexToRgba("#0E0C1F"), + ChildBg: ColourUtil.HexToRgba("#15122B"), + FrameBg: ColourUtil.HexToRgba("#1F1A38"), + Surface: ColourUtil.HexToRgba("#28224A"), + SurfaceHover: ColourUtil.HexToRgba("#332B5B"), + Border: ColourUtil.HexToRgba("#E374E844"), + + TextPrimary: ColourUtil.HexToRgba("#ECE6F5"), + TextMuted: ColourUtil.HexToRgba("#9A8BB0"), + TextDim: ColourUtil.HexToRgba("#554B6E"), + + StatusSuccess: ColourUtil.HexToRgba("#7AAC5C"), + StatusDanger: ColourUtil.HexToRgba("#E85C6A"), + StatusWarning: ColourUtil.HexToRgba("#E8B590"), + StatusInfo: ColourUtil.HexToRgba("#6278FF") + ), + Layout: new ThemeLayout( + WindowRounding: 6f, ChildRounding: 5f, PopupRounding: 5f, + FrameRounding: 4f, GrabRounding: 4f, TabRounding: 4f, + ScrollbarRounding: 4f, WindowBorderSize: 1f, FrameBorderSize: 1f + ), + Typography: new ThemeTypography(), + IsBuiltIn: true + ); +} -- 2.52.0 From 4c6d52e6520293a4d5a573c0265dde0b4fd19ed1 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 10:25:22 +0200 Subject: [PATCH 033/169] feat(themes): mint-grove built-in theme --- HellionChat/Themes/Builtin/MintGrove.cs | 50 +++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 HellionChat/Themes/Builtin/MintGrove.cs diff --git a/HellionChat/Themes/Builtin/MintGrove.cs b/HellionChat/Themes/Builtin/MintGrove.cs new file mode 100644 index 0000000..727d30c --- /dev/null +++ b/HellionChat/Themes/Builtin/MintGrove.cs @@ -0,0 +1,50 @@ +using HellionChat.Util; + +namespace HellionChat.Themes.Builtin; + +internal static class MintGrove +{ + public const string Slug = "mint-grove"; + + public static Theme Build() => new( + Slug: Slug, + Name: "Mint Grove", + Author: "Hellion Online Media", + Description: "Mint Green + Honey Amber auf Deep Forest. Naturthemen-tauglich.", + Colors: new ThemeColors( + PrimaryDark: ColourUtil.HexToRgba("#3CB371"), + Primary: ColourUtil.HexToRgba("#5DD39E"), + PrimaryLight: ColourUtil.HexToRgba("#8FE0B8"), + PrimaryGlow: ColourUtil.HexToRgba("#5DD39E99"), + + AccentDark: ColourUtil.HexToRgba("#F4C870"), + Accent: ColourUtil.HexToRgba("#F9D580"), + AccentLight: ColourUtil.HexToRgba("#FCDD93"), + + Identity: ColourUtil.HexToRgba("#5DD39E"), + + WindowBg: ColourUtil.HexToRgba("#0A1410"), + ChildBg: ColourUtil.HexToRgba("#10201A"), + FrameBg: ColourUtil.HexToRgba("#162B22"), + Surface: ColourUtil.HexToRgba("#1E372B"), + SurfaceHover: ColourUtil.HexToRgba("#284335"), + Border: ColourUtil.HexToRgba("#5DD39E55"), + + TextPrimary: ColourUtil.HexToRgba("#E8F5EA"), + TextMuted: ColourUtil.HexToRgba("#9BB5A5"), + TextDim: ColourUtil.HexToRgba("#5C6F65"), + + StatusSuccess: ColourUtil.HexToRgba("#5DD39E"), + StatusDanger: ColourUtil.HexToRgba("#D9534F"), + StatusWarning: ColourUtil.HexToRgba("#E8B590"), + StatusInfo: ColourUtil.HexToRgba("#5DA9C7") + ), + Layout: new ThemeLayout( + WindowRounding: 5f, ChildRounding: 4f, PopupRounding: 4f, + FrameRounding: 3f, GrabRounding: 3f, TabRounding: 3f, + ScrollbarRounding: 3f, WindowBorderSize: 1f, FrameBorderSize: 1f + ), + Typography: new ThemeTypography(), + IsBuiltIn: true + ); +} -- 2.52.0 From cae7d762069c437c7c93d6bf84344beaf0bd7350 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 10:27:50 +0200 Subject: [PATCH 034/169] feat(themes): theme registry with built-in lookup and fallback --- HellionChat/Themes/ThemeRegistry.cs | 42 +++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 HellionChat/Themes/ThemeRegistry.cs diff --git a/HellionChat/Themes/ThemeRegistry.cs b/HellionChat/Themes/ThemeRegistry.cs new file mode 100644 index 0000000..efb1768 --- /dev/null +++ b/HellionChat/Themes/ThemeRegistry.cs @@ -0,0 +1,42 @@ +using HellionChat.Themes.Builtin; + +namespace HellionChat.Themes; + +public sealed class ThemeRegistry +{ + public const string DefaultSlug = HellionArctic.Slug; + + private readonly Dictionary _builtIns; + private Theme _active; + + public ThemeRegistry() + { + _builtIns = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { HellionArctic.Slug, HellionArctic.Build() }, + { Chat2Classic.Slug, Chat2Classic.Build() }, + { EventHorizon.Slug, EventHorizon.Build() }, + { MoonlitBloom.Slug, MoonlitBloom.Build() }, + { MintGrove.Slug, MintGrove.Build() }, + }; + _active = _builtIns[DefaultSlug]; + } + + public Theme Active => _active; + + // Active-Lookup: liefert das angeforderte Theme oder fällt auf Hellion Arctic + // zurück, wenn der Slug unbekannt ist. + public Theme Get(string slug) + { + return _builtIns.TryGetValue(slug, out var theme) + ? theme + : _builtIns[DefaultSlug]; + } + + public IEnumerable AllBuiltIns() => _builtIns.Values; + + public void Switch(string slug) + { + _active = Get(slug); + } +} -- 2.52.0 From b85db2460106827e5e216e70def31fa0f82754ab Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 10:28:08 +0200 Subject: [PATCH 035/169] test(themes): sanity tests for all built-in themes -- 2.52.0 From 2378ce6bf2de9bf37d3d666391de3d0dfbfac96e Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 13:42:40 +0200 Subject: [PATCH 036/169] feat(themes): json loader with schema validation --- HellionChat/Themes/ThemeJsonLoader.cs | 106 ++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 HellionChat/Themes/ThemeJsonLoader.cs diff --git a/HellionChat/Themes/ThemeJsonLoader.cs b/HellionChat/Themes/ThemeJsonLoader.cs new file mode 100644 index 0000000..d73200f --- /dev/null +++ b/HellionChat/Themes/ThemeJsonLoader.cs @@ -0,0 +1,106 @@ +using System.Text.Json; +using HellionChat.Util; + +namespace HellionChat.Themes; + +internal static class ThemeJsonLoader +{ + public const int SupportedSchemaVersion = 1; + + public static Theme LoadFromString(string json) + { + if (string.IsNullOrWhiteSpace(json)) + throw new FormatException("Theme JSON is empty"); + + JsonDocument doc; + try { doc = JsonDocument.Parse(json); } + catch (JsonException ex) { throw new FormatException("Theme JSON is not valid JSON", ex); } + + using (doc) + { + var root = doc.RootElement; + + var schemaVersion = ReadInt(root, "schemaVersion"); + if (schemaVersion != SupportedSchemaVersion) + throw new FormatException($"Unsupported schemaVersion {schemaVersion}; expected {SupportedSchemaVersion}"); + + var slug = ReadString(root, "slug"); + var name = ReadString(root, "name"); + var author = ReadString(root, "author"); + var description = ReadString(root, "description"); + + var colors = ReadColors(root.GetProperty("colors")); + var layout = ReadLayout(root.GetProperty("layout")); + + return new Theme(slug, name, author, description, colors, layout, new ThemeTypography(), IsBuiltIn: false); + } + } + + public static Theme LoadFromFile(string path) + { + var json = File.ReadAllText(path); + return LoadFromString(json); + } + + private static ThemeColors ReadColors(JsonElement el) => new( + PrimaryDark: ColourUtil.HexToRgba(ReadString(el, "primaryDark")), + Primary: ColourUtil.HexToRgba(ReadString(el, "primary")), + PrimaryLight: ColourUtil.HexToRgba(ReadString(el, "primaryLight")), + PrimaryGlow: ColourUtil.HexToRgba(ReadString(el, "primaryGlow")), + + AccentDark: ColourUtil.HexToRgba(ReadString(el, "accentDark")), + Accent: ColourUtil.HexToRgba(ReadString(el, "accent")), + AccentLight: ColourUtil.HexToRgba(ReadString(el, "accentLight")), + + Identity: ColourUtil.HexToRgba(ReadString(el, "identity")), + + WindowBg: ColourUtil.HexToRgba(ReadString(el, "windowBg")), + ChildBg: ColourUtil.HexToRgba(ReadString(el, "childBg")), + FrameBg: ColourUtil.HexToRgba(ReadString(el, "frameBg")), + Surface: ColourUtil.HexToRgba(ReadString(el, "surface")), + SurfaceHover: ColourUtil.HexToRgba(ReadString(el, "surfaceHover")), + Border: ColourUtil.HexToRgba(ReadString(el, "border")), + + TextPrimary: ColourUtil.HexToRgba(ReadString(el, "textPrimary")), + TextMuted: ColourUtil.HexToRgba(ReadString(el, "textMuted")), + TextDim: ColourUtil.HexToRgba(ReadString(el, "textDim")), + + StatusSuccess: ColourUtil.HexToRgba(ReadString(el, "statusSuccess")), + StatusDanger: ColourUtil.HexToRgba(ReadString(el, "statusDanger")), + StatusWarning: ColourUtil.HexToRgba(ReadString(el, "statusWarning")), + StatusInfo: ColourUtil.HexToRgba(ReadString(el, "statusInfo")) + ); + + private static ThemeLayout ReadLayout(JsonElement el) => new( + WindowRounding: ReadFloat(el, "windowRounding"), + ChildRounding: ReadFloat(el, "childRounding"), + PopupRounding: ReadFloat(el, "popupRounding"), + FrameRounding: ReadFloat(el, "frameRounding"), + GrabRounding: ReadFloat(el, "grabRounding"), + TabRounding: ReadFloat(el, "tabRounding"), + ScrollbarRounding: ReadFloat(el, "scrollbarRounding"), + WindowBorderSize: ReadFloat(el, "windowBorderSize"), + FrameBorderSize: ReadFloat(el, "frameBorderSize") + ); + + private static string ReadString(JsonElement el, string name) + { + if (!el.TryGetProperty(name, out var v) || v.ValueKind != JsonValueKind.String) + throw new FormatException($"Theme JSON missing string property '{name}'"); + return v.GetString() ?? throw new FormatException($"Theme JSON property '{name}' is null"); + } + + private static int ReadInt(JsonElement el, string name) + { + if (!el.TryGetProperty(name, out var v) || v.ValueKind != JsonValueKind.Number) + throw new FormatException($"Theme JSON missing number property '{name}'"); + return v.GetInt32(); + } + + private static float ReadFloat(JsonElement el, string name) + { + if (!el.TryGetProperty(name, out var v) || v.ValueKind != JsonValueKind.Number) + throw new FormatException($"Theme JSON missing number property '{name}'"); + return (float)v.GetDouble(); + } +} -- 2.52.0 From 4bf6c3ef1f6276e994df5bfd85c2143755feeb53 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 13:44:15 +0200 Subject: [PATCH 037/169] feat(themes): custom theme loading with file-stamp cache --- HellionChat/Themes/ThemeRegistry.cs | 70 +++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/HellionChat/Themes/ThemeRegistry.cs b/HellionChat/Themes/ThemeRegistry.cs index efb1768..3e14c84 100644 --- a/HellionChat/Themes/ThemeRegistry.cs +++ b/HellionChat/Themes/ThemeRegistry.cs @@ -7,9 +7,11 @@ public sealed class ThemeRegistry public const string DefaultSlug = HellionArctic.Slug; private readonly Dictionary _builtIns; + private readonly Dictionary _customCache = new(StringComparer.OrdinalIgnoreCase); + private readonly string? _customThemesDir; private Theme _active; - public ThemeRegistry() + public ThemeRegistry(string? customThemesDir = null) { _builtIns = new Dictionary(StringComparer.OrdinalIgnoreCase) { @@ -20,23 +22,75 @@ public sealed class ThemeRegistry { MintGrove.Slug, MintGrove.Build() }, }; _active = _builtIns[DefaultSlug]; + _customThemesDir = customThemesDir; } public Theme Active => _active; - // Active-Lookup: liefert das angeforderte Theme oder fällt auf Hellion Arctic - // zurück, wenn der Slug unbekannt ist. public Theme Get(string slug) { - return _builtIns.TryGetValue(slug, out var theme) - ? theme - : _builtIns[DefaultSlug]; + if (_builtIns.TryGetValue(slug, out var b)) return b; + + var custom = LoadCustomBySlug(slug); + if (custom != null) return custom; + + return _builtIns[DefaultSlug]; } public IEnumerable AllBuiltIns() => _builtIns.Values; - public void Switch(string slug) + public IEnumerable AllCustom() => RefreshCustomCache(); + + public void Switch(string slug) => _active = Get(slug); + + // Custom-Themes werden lazy aus dem Verzeichnis geladen, Cache mit + // LastWriteTime-Token. Eine geänderte JSON wird beim nächsten Lookup + // neu eingelesen. + private Theme? LoadCustomBySlug(string slug) { - _active = Get(slug); + if (_customThemesDir is null) return null; + if (!Directory.Exists(_customThemesDir)) return null; + + foreach (var theme in RefreshCustomCache()) + if (string.Equals(theme.Slug, slug, StringComparison.OrdinalIgnoreCase)) + return theme; + return null; + } + + private IEnumerable RefreshCustomCache() + { + if (_customThemesDir is null || !Directory.Exists(_customThemesDir)) + yield break; + + var seenSlugs = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var path in Directory.EnumerateFiles(_customThemesDir, "*.json")) + { + Theme? theme = null; + var stamp = File.GetLastWriteTimeUtc(path); + var key = path; + if (_customCache.TryGetValue(key, out var cached) && cached.Stamp == stamp) + { + theme = cached.Theme; + } + else + { + try + { + theme = ThemeJsonLoader.LoadFromFile(path); + _customCache[key] = (theme, stamp); + } + catch (Exception ex) + { + // Logging passiert in Plugin.cs durch den Aufrufer; hier still + // ignorieren, damit ein einzelnes kaputtes JSON nicht alle + // Custom-Themes blockt. + _ = ex; + continue; + } + } + + if (theme is not null && seenSlugs.Add(theme.Slug)) + yield return theme; + } } } -- 2.52.0 From dd3a0ea0692a0ac8382dd4260566ba256b0dec3f Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 13:51:31 +0200 Subject: [PATCH 038/169] =?UTF-8?q?feat(themes):=20wire=20theme=20engine?= =?UTF-8?q?=20into=20plugin=20draw=20pipeline=20+=20migrate=20v13=E2=86=92?= =?UTF-8?q?v14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HellionStyle.PushGlobal nimmt jetzt eine Theme-Instance + Window-Opacity und liest alle Color- und Style-Slots aus dem aktiven Theme statt aus einer fixen Konstanten-Tabelle. Plugin hält die ThemeRegistry und schaltet beim Init auf das in Config.Theme gespeicherte Slug. Configuration v13 → v14: - Neue Felder Theme (slug), WindowOpacity, ReduceMotion, UseCompactDensity, ShowThemeQuickPicker - HellionThemeEnabled und HellionThemeWindowOpacity sind ab v14 [Obsolete] und bleiben bis v1.2.0 als JSON-Safety-Net erhalten - Migration setzt alle Bestandsuser auf hellion-arctic; chat2-classic bleibt im Themes-Tab als Upstream-Look wählbar - WindowOpacity übernimmt den Wert von HellionThemeWindowOpacity, alte HellionThemeEnabled-Flag entfällt funktional (Theme-Engine ist immer aktiv) Konsumenten der alten Felder (ChatLogWindow.BgAlpha, Popout.BgAlpha) lesen jetzt das neue WindowOpacity. Die Settings-UI in Appearance.cs schreibt übergangsweise weiter in die Obsolete-Felder; Phase J ersetzt diesen Block durch den dedizierten Themes-Tab. CS0612/CS0618 sind dort gezielt mit pragma gekapselt. --- HellionChat/Configuration.cs | 26 ++- HellionChat/Plugin.cs | 39 +++- HellionChat/Ui/ChatLogWindow.cs | 4 +- HellionChat/Ui/HellionStyle.cs | 259 +++++++--------------- HellionChat/Ui/Popout.cs | 4 +- HellionChat/Ui/SettingsTabs/Appearance.cs | 8 + 6 files changed, 153 insertions(+), 187 deletions(-) diff --git a/HellionChat/Configuration.cs b/HellionChat/Configuration.cs index fbe7fdc..da3b4d6 100755 --- a/HellionChat/Configuration.cs +++ b/HellionChat/Configuration.cs @@ -34,10 +34,23 @@ public class ConfigKeyBind [Serializable] public class Configuration : IPluginConfiguration { - private const int LatestVersion = 13; + private const int LatestVersion = 14; public int Version { get; set; } = LatestVersion; + // v1.1.0 — Theme-Engine. Slug-basiert; ThemeRegistry liefert das Objekt. + public string Theme = "hellion-arctic"; + + // v1.1.0 — Globale Window-Opacity, theme-übergreifend. Migration aus + // HellionThemeWindowOpacity beim Bump v13 → v14. + public float WindowOpacity = 0.85f; + + // v1.1.0 — Felder für künftige UI-Toggles (v1.2.0 / v1.3.0). Werden + // vorab angelegt, damit später keine Migration nötig ist. + public bool ReduceMotion; + public bool UseCompactDensity; + public bool ShowThemeQuickPicker; + // Hellion Chat — Privacy filter (DSGVO Art. 25 Privacy by Default). // Master-switch defaults to true; set false to restore upstream behavior. public bool PrivacyFilterEnabled = true; @@ -70,12 +83,14 @@ public class Configuration : IPluginConfiguration // Hellion Chat global ImGui theme — applied to every plugin window in // Plugin.Draw. Default ON; users who prefer the upstream Dalamud look // can flip this off in the Privacy tab. + [Obsolete("Replaced by Theme slug + WindowOpacity in v14")] public bool HellionThemeEnabled = true; // Window background opacity, 0.5–1.0. Lower values make the plugin // panes more glass-like so the game shines through. Default 0.5 // matches the maintainer's daily-driver preference; users who want // a less translucent look bump it up in Aussehen → Theme. + [Obsolete("Replaced by WindowOpacity in v14")] public float HellionThemeWindowOpacity = 0.5f; // Use the bundled Exo 2 font (OFL-1.1) for the regular plugin font @@ -321,10 +336,19 @@ public class Configuration : IPluginConfiguration RetentionLastRunAt = other.RetentionLastRunAt; FirstRunCompleted = other.FirstRunCompleted; +#pragma warning disable CS0612, CS0618 // Obsolete-Felder bleiben bis v1.2.0 als JSON-Safety-Net erhalten HellionThemeEnabled = other.HellionThemeEnabled; HellionThemeWindowOpacity = other.HellionThemeWindowOpacity; +#pragma warning restore CS0612, CS0618 UseHellionFont = other.UseHellionFont; + // v1.1.0 theme engine fields + Theme = other.Theme; + WindowOpacity = other.WindowOpacity; + ReduceMotion = other.ReduceMotion; + UseCompactDensity = other.UseCompactDensity; + ShowThemeQuickPicker = other.ShowThemeQuickPicker; + EnableAutoTellTabs = other.EnableAutoTellTabs; AutoTellTabsLimit = other.AutoTellTabsLimit; AutoTellTabsCompactDisplay = other.AutoTellTabsCompactDisplay; diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index 992600a..e5e7770 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -63,6 +63,7 @@ public sealed class Plugin : IDalamudPlugin internal ExtraChat ExtraChat { get; } internal TypingIpc TypingIpc { get; } internal FontManager FontManager { get; } + internal Themes.ThemeRegistry ThemeRegistry { get; private set; } = null!; internal int DeferredSaveFrames = -1; @@ -237,6 +238,27 @@ public sealed class Plugin : IDalamudPlugin }); } + // Hellion Chat v13 → v14 — theme-engine migration. Alle User landen + // auf "hellion-arctic" als neues Default-Theme; die alte + // HellionThemeEnabled-Flag wird deprecated und nur noch ein Release + // als Safety-Net im JSON behalten. Window-Opacity wandert von + // HellionThemeWindowOpacity in das neue WindowOpacity-Feld. + if (Config.Version < 14) + { + Config.Theme = "hellion-arctic"; + #pragma warning disable CS0612, CS0618 // Obsolete: HellionThemeWindowOpacity bleibt readable bis v1.2.0 + Config.WindowOpacity = Config.HellionThemeWindowOpacity; + #pragma warning restore CS0612, CS0618 + Config.ReduceMotion = false; + Config.UseCompactDensity = false; + Config.ShowThemeQuickPicker = false; + Config.Version = 14; + SaveConfig(); + Log.Information( + "Migrated config v13 → v14: theme engine introduced, all users land on hellion-arctic; " + + "pick chat2-classic in Settings → Themes for the upstream look"); + } + // Hellion v1.0.0 default tab layout. Five thematically separated // tabs: General catches the immediate-surroundings public chat // (Say/Yell/Shout) only; System absorbs the rest of the technical @@ -266,6 +288,12 @@ public sealed class Plugin : IDalamudPlugin ExtraChat = new ExtraChat(); FontManager = new FontManager(); + // v1.1.0 — Theme-Engine init. Custom-Themes liegen in + // pluginConfigs/HellionChat/themes/, lazy geladen beim ersten Get. + var customThemesDir = Path.Combine(Interface.ConfigDirectory.FullName, "themes"); + ThemeRegistry = new Themes.ThemeRegistry(customThemesDir); + ThemeRegistry.Switch(Config.Theme); + MessageManager = new MessageManager(this); // Does it require UI? // Hellion Chat — Auto-Tell-Tabs service. Subscribes to the @@ -559,13 +587,10 @@ public sealed class Plugin : IDalamudPlugin private void Draw() { - // Hellion theme is pushed once per frame here so every plugin window - // (chat log, settings, viewers, wizard, file dialog) renders with - // the same palette. Skipping the push leaves the upstream Dalamud - // look untouched for users who flipped the toggle off. - using IDisposable? _style = Config.HellionThemeEnabled - ? HellionStyle.PushGlobal(Config.HellionThemeWindowOpacity) - : null; + // Theme-Engine ist ab v14 immer aktiv; Klassik ist jetzt ein eigenes + // Theme statt einem deaktivierten Hellion-Theme. Active wird einmal + // pro Frame aus der Registry gelesen. + using IDisposable _style = HellionStyle.PushGlobal(ThemeRegistry.Active, Config.WindowOpacity); ChatLogWindow.BeginFrame(); diff --git a/HellionChat/Ui/ChatLogWindow.cs b/HellionChat/Ui/ChatLogWindow.cs index cd7c8db..f5fda68 100644 --- a/HellionChat/Ui/ChatLogWindow.cs +++ b/HellionChat/Ui/ChatLogWindow.cs @@ -494,9 +494,7 @@ public sealed class ChatLogWindow : Window Flags |= ImGuiWindowFlags.NoTitleBar; if (LastViewport == ImGuiHelpers.MainViewport.Handle && !WasDocked) - BgAlpha = Plugin.Config.HellionThemeEnabled - ? Plugin.Config.HellionThemeWindowOpacity - : Plugin.Config.WindowAlpha / 100f; + BgAlpha = Plugin.Config.WindowOpacity; LastViewport = ImGui.GetWindowViewport().Handle; WasDocked = ImGui.IsWindowDocked(); diff --git a/HellionChat/Ui/HellionStyle.cs b/HellionChat/Ui/HellionStyle.cs index 026c464..39d839b 100644 --- a/HellionChat/Ui/HellionStyle.cs +++ b/HellionChat/Ui/HellionStyle.cs @@ -1,3 +1,4 @@ +using HellionChat.Themes; using HellionChat.Util; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Utility.Raii; @@ -5,207 +6,119 @@ using Dalamud.Interface.Utility.Raii; namespace HellionChat.Ui; /// -/// ImGui style override for Hellion Chat. Industrial HUD palette with three -/// distinct accents — cyan-teal as the primary action color, industrial -/// amber for active state highlights, slate-violet for title bars and -/// active tabs — on a deep-slate frame background with steel borders. -/// -/// Two entry points: -/// Push — local color stack, scoped via using-block. Use inside -/// Hellion-only surfaces (Privacy tab, first-run wizard). -/// PushGlobal — full color + style variable stack. Pushed once per frame -/// in Plugin.Draw so every Hellion-rendered window inherits -/// the look. Cheap to pop because ImGui keeps its own stack. +/// ImGui style override for Hellion Chat. v1.1.0 ist die Engine +/// theme-getrieben: PushGlobal nimmt eine Theme-Instance + Window- +/// Opacity, die gesamten Color- und Style-Slots werden aus dem Theme +/// gelesen statt aus einer fixen Konstanten-Tabelle. /// internal static class HellionStyle { - // Encoded as 0xRRGGBBAA, matching ChatTwo convention (see Settings.cs - // Ko-fi buttons). RgbaToAbgr handles the byte swap to the format ImGui - // expects. Hex values are sourced from the Hellion Online Media brand - // guide ("Arctic Cyan + Ember Glow", BRANDING.md in the website repo). - - // Primary — Arctic Cyan, used for every interactive control (buttons, - // checks, sliders, separators when hovered). Three brand stages plus a - // hover that lifts to brand-color-light and a press that drops to - // brand-color-dark. - private const uint PrimaryRgba = 0x00BED2FF; // brand-color - private const uint PrimaryHoverRgba = 0x4DD9E8FF; // brand-color-light - private const uint PrimaryActiveRgba = 0x0097A7FF; // brand-color-dark - - // Identity — brand-color-dark teal for window title bars and the - // active tab. Sits visibly below the primary cyan on buttons so the - // user sees "where am I" (deep teal) versus "what can I click" - // (brand cyan) without leaving the cyan family. - private const uint IdentityRgba = 0x0097A7FF; // brand-color-dark - private const uint IdentityHoverRgba = 0x4DD9E8FF; // brand-color-light - private const uint IdentityDeepRgba = 0x005670FF; // dimmer teal for unfocused-active tab - - // Accent — Ember Orange for warm highlights on grips and scrollbar - // pulls. Replaces the previous industrial amber so the plugin matches - // the website's CTA palette. AccentActive is reserved for any future - // pressed-state on accent surfaces; the current slots only need - // AccentRgba and AccentHoverRgba. - private const uint AccentRgba = 0xF97316FF; // accent-color - private const uint AccentHoverRgba = 0xFB923CFF; // accent-color-light - - // Surfaces — Hellion brand background ladder. Window darkest, frame - // hover ladder climbs into surface tones. Matches the website's - // background / background-medium / background-light / surface vars. - private const uint WindowBgRgba = 0x070B12FF; // background - private const uint ChildBgRgba = 0x0C1220FF; // background-medium - private const uint PopupBgRgba = 0x0C1220FF; // background-medium - private const uint FrameBgRgba = 0x141E30FF; // background-light - private const uint FrameBgHoverRgba = 0x1A2538FF; // surface - private const uint FrameBgActiveRgba = 0x22303FFF; // surface-hover - // Cyan-tinted border — matches website --border-brand (cyan @ 40% α). - private const uint BorderRgba = 0x00BED266; - private const uint BorderShadowRgba = 0x00000000; - - // Headers / collapsing-headers / tree nodes / selectables — same - // surface ladder as frames so panels feel consistent. - private const uint HeaderRgba = 0x141E30FF; - private const uint HeaderHoverRgba = 0x1A2538FF; - private const uint HeaderActiveRgba = 0x22303FFF; - - // Title bars — Identity teal on active so the focused window reads - // as "yours" without using accent or primary slots. - private const uint TitleBgRgba = 0x070B12FF; - private const uint TitleBgActiveRgba = IdentityRgba; - private const uint TitleBgCollapsedRgba = 0x05080EFF; - - // Tabs — neutral inactive, Identity-light on hover, Identity teal on - // active. Unfocused-active uses the deeper Identity stage so an - // unfocused window's active tab still reads but does not pull focus. - private const uint TabRgba = 0x141E30FF; - private const uint TabHoveredRgba = IdentityHoverRgba; - private const uint TabActiveRgba = IdentityRgba; - private const uint TabUnfocusedRgba = 0x0C1220FF; - private const uint TabUnfocusedActiveRgba = IdentityDeepRgba; - - // Scrollbar — Ember on grab so the pull stands out without competing - // with the cyan action buttons. Idle grab is a subtle surface tone, - // hover/active climb into accent. - private const uint ScrollbarBgRgba = 0x070B12FF; - private const uint ScrollbarGrabRgba = 0x22303FFF; // surface-hover - private const uint ScrollbarGrabHoveredRgba = AccentHoverRgba; - private const uint ScrollbarGrabActiveRgba = AccentRgba; - - // Resize grip — same Ember treatment as the scrollbar. - private const uint ResizeGripRgba = 0x141E30FF; - private const uint ResizeGripHoveredRgba = AccentHoverRgba; - private const uint ResizeGripActiveRgba = AccentRgba; - - // Separator and check mark / slider follow the primary cyan. - /// - /// Local color stack for Hellion-only surfaces. Cheap. Use inside a - /// `using var _ = HellionStyle.Push();` block. + /// Local color stack auf Basis des aktiven Themes. Cheap. Use inside a + /// `using var _ = HellionStyle.Push(theme);` block. /// - internal static IDisposable Push() + internal static IDisposable Push(Theme theme) { + var c = theme.Colors; var stack = new StackHandle(); - stack.PushColor(ImGuiCol.Button, PrimaryRgba); - stack.PushColor(ImGuiCol.ButtonHovered, PrimaryHoverRgba); - stack.PushColor(ImGuiCol.ButtonActive, PrimaryActiveRgba); - stack.PushColor(ImGuiCol.FrameBg, FrameBgRgba); - stack.PushColor(ImGuiCol.FrameBgHovered, FrameBgHoverRgba); - stack.PushColor(ImGuiCol.FrameBgActive, FrameBgActiveRgba); - stack.PushColor(ImGuiCol.Border, BorderRgba); - stack.PushColor(ImGuiCol.Header, HeaderRgba); - stack.PushColor(ImGuiCol.HeaderHovered, HeaderHoverRgba); - stack.PushColor(ImGuiCol.HeaderActive, HeaderActiveRgba); - stack.PushColor(ImGuiCol.CheckMark, PrimaryRgba); - stack.PushColor(ImGuiCol.SliderGrab, PrimaryRgba); - stack.PushColor(ImGuiCol.SliderGrabActive, PrimaryHoverRgba); + stack.PushColor(ImGuiCol.Button, c.Primary); + stack.PushColor(ImGuiCol.ButtonHovered, c.PrimaryLight); + stack.PushColor(ImGuiCol.ButtonActive, c.PrimaryDark); + stack.PushColor(ImGuiCol.FrameBg, c.FrameBg); + stack.PushColor(ImGuiCol.FrameBgHovered, c.SurfaceHover); + stack.PushColor(ImGuiCol.FrameBgActive, c.Surface); + stack.PushColor(ImGuiCol.Border, c.Border); + stack.PushColor(ImGuiCol.Header, c.Surface); + stack.PushColor(ImGuiCol.HeaderHovered, c.SurfaceHover); + stack.PushColor(ImGuiCol.HeaderActive, c.Identity); + stack.PushColor(ImGuiCol.CheckMark, c.Primary); + stack.PushColor(ImGuiCol.SliderGrab, c.Primary); + stack.PushColor(ImGuiCol.SliderGrabActive, c.PrimaryLight); return stack; } /// /// Global color and style-variable stack pushed once per frame in - /// Plugin.Draw. Covers every ImGui surface the plugin renders so the - /// Hellion look is consistent across upstream and Hellion tabs. + /// Plugin.Draw. Drives every Hellion-rendered window from the active + /// theme's palette and layout values. /// - /// Window background alpha (0.5–1.0). Lower - /// values let the game shine through the plugin panes. - internal static IDisposable PushGlobal(float windowOpacity = 1.0f) + /// Active theme from ThemeRegistry. + /// Window background alpha (0.5–1.0). + internal static IDisposable PushGlobal(Theme theme, float windowOpacity = 1.0f) { + var c = theme.Colors; + var l = theme.Layout; var stack = new StackHandle(); - // Mix the configured opacity into both the outer window and the - // inner content child backgrounds — without ChildBg following the - // slider the chat log stays opaque inside even when the user - // wants to see the game behind it during combat. Form fields and - // popups (FrameBg, PopupBg) still stay opaque so input is readable. var alphaByte = (uint)Math.Clamp((int)(windowOpacity * 255f), 0x55, 0xFF); - var windowBgWithAlpha = (WindowBgRgba & 0xFFFFFF00u) | alphaByte; - var childBgWithAlpha = (ChildBgRgba & 0xFFFFFF00u) | alphaByte; + var windowBgWithAlpha = (c.WindowBg & 0xFFFFFF00u) | alphaByte; + var childBgWithAlpha = (c.ChildBg & 0xFFFFFF00u) | alphaByte; - // Layout — geometric edges, modest rounding, single-pixel borders. - stack.PushStyleVar(ImGuiStyleVar.WindowRounding, 4f); - stack.PushStyleVar(ImGuiStyleVar.ChildRounding, 3f); - stack.PushStyleVar(ImGuiStyleVar.PopupRounding, 3f); - stack.PushStyleVar(ImGuiStyleVar.FrameRounding, 2f); - stack.PushStyleVar(ImGuiStyleVar.GrabRounding, 2f); - stack.PushStyleVar(ImGuiStyleVar.TabRounding, 2f); - stack.PushStyleVar(ImGuiStyleVar.ScrollbarRounding, 2f); - stack.PushStyleVar(ImGuiStyleVar.WindowBorderSize, 1f); - stack.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1f); + // Layout + stack.PushStyleVar(ImGuiStyleVar.WindowRounding, l.WindowRounding); + stack.PushStyleVar(ImGuiStyleVar.ChildRounding, l.ChildRounding); + stack.PushStyleVar(ImGuiStyleVar.PopupRounding, l.PopupRounding); + stack.PushStyleVar(ImGuiStyleVar.FrameRounding, l.FrameRounding); + stack.PushStyleVar(ImGuiStyleVar.GrabRounding, l.GrabRounding); + stack.PushStyleVar(ImGuiStyleVar.TabRounding, l.TabRounding); + stack.PushStyleVar(ImGuiStyleVar.ScrollbarRounding, l.ScrollbarRounding); + stack.PushStyleVar(ImGuiStyleVar.WindowBorderSize, l.WindowBorderSize); + stack.PushStyleVar(ImGuiStyleVar.FrameBorderSize, l.FrameBorderSize); - // Surfaces. - stack.PushColor(ImGuiCol.WindowBg, windowBgWithAlpha); - stack.PushColor(ImGuiCol.ChildBg, childBgWithAlpha); - stack.PushColor(ImGuiCol.PopupBg, PopupBgRgba); - stack.PushColor(ImGuiCol.Border, BorderRgba); - stack.PushColor(ImGuiCol.BorderShadow, BorderShadowRgba); + // Surfaces + stack.PushColor(ImGuiCol.WindowBg, windowBgWithAlpha); + stack.PushColor(ImGuiCol.ChildBg, childBgWithAlpha); + stack.PushColor(ImGuiCol.PopupBg, c.ChildBg); + stack.PushColor(ImGuiCol.Border, c.Border); + stack.PushColor(ImGuiCol.BorderShadow, 0u); - // Frames (input fields, combos, sliders). - stack.PushColor(ImGuiCol.FrameBg, FrameBgRgba); - stack.PushColor(ImGuiCol.FrameBgHovered, FrameBgHoverRgba); - stack.PushColor(ImGuiCol.FrameBgActive, FrameBgActiveRgba); + // Frames + stack.PushColor(ImGuiCol.FrameBg, c.FrameBg); + stack.PushColor(ImGuiCol.FrameBgHovered, c.SurfaceHover); + stack.PushColor(ImGuiCol.FrameBgActive, c.Surface); - // Title bars — tertiary identity on active. - stack.PushColor(ImGuiCol.TitleBg, TitleBgRgba); - stack.PushColor(ImGuiCol.TitleBgActive, TitleBgActiveRgba); - stack.PushColor(ImGuiCol.TitleBgCollapsed, TitleBgCollapsedRgba); + // Title bars + stack.PushColor(ImGuiCol.TitleBg, c.WindowBg); + stack.PushColor(ImGuiCol.TitleBgActive, c.Identity); + stack.PushColor(ImGuiCol.TitleBgCollapsed, c.WindowBg); - // Buttons — primary cyan. - stack.PushColor(ImGuiCol.Button, PrimaryRgba); - stack.PushColor(ImGuiCol.ButtonHovered, PrimaryHoverRgba); - stack.PushColor(ImGuiCol.ButtonActive, PrimaryActiveRgba); + // Buttons + stack.PushColor(ImGuiCol.Button, c.Primary); + stack.PushColor(ImGuiCol.ButtonHovered, c.PrimaryLight); + stack.PushColor(ImGuiCol.ButtonActive, c.PrimaryDark); - // Headers / selectables — slate with subtle steps. - stack.PushColor(ImGuiCol.Header, HeaderRgba); - stack.PushColor(ImGuiCol.HeaderHovered, HeaderHoverRgba); - stack.PushColor(ImGuiCol.HeaderActive, HeaderActiveRgba); + // Headers / selectables + stack.PushColor(ImGuiCol.Header, c.Surface); + stack.PushColor(ImGuiCol.HeaderHovered, c.SurfaceHover); + stack.PushColor(ImGuiCol.HeaderActive, c.Identity); - // Tabs — tertiary identity for the active tab. - stack.PushColor(ImGuiCol.Tab, TabRgba); - stack.PushColor(ImGuiCol.TabHovered, TabHoveredRgba); - stack.PushColor(ImGuiCol.TabActive, TabActiveRgba); - stack.PushColor(ImGuiCol.TabUnfocused, TabUnfocusedRgba); - stack.PushColor(ImGuiCol.TabUnfocusedActive, TabUnfocusedActiveRgba); + // Tabs + stack.PushColor(ImGuiCol.Tab, c.FrameBg); + stack.PushColor(ImGuiCol.TabHovered, c.PrimaryLight); + stack.PushColor(ImGuiCol.TabActive, c.Identity); + stack.PushColor(ImGuiCol.TabUnfocused, c.ChildBg); + stack.PushColor(ImGuiCol.TabUnfocusedActive, c.PrimaryDark); - // Scrollbar. - stack.PushColor(ImGuiCol.ScrollbarBg, ScrollbarBgRgba); - stack.PushColor(ImGuiCol.ScrollbarGrab, ScrollbarGrabRgba); - stack.PushColor(ImGuiCol.ScrollbarGrabHovered, ScrollbarGrabHoveredRgba); - stack.PushColor(ImGuiCol.ScrollbarGrabActive, ScrollbarGrabActiveRgba); + // Scrollbar + stack.PushColor(ImGuiCol.ScrollbarBg, c.WindowBg); + stack.PushColor(ImGuiCol.ScrollbarGrab, c.Surface); + stack.PushColor(ImGuiCol.ScrollbarGrabHovered, c.AccentLight); + stack.PushColor(ImGuiCol.ScrollbarGrabActive, c.Accent); - // Resize grip — secondary amber on active. - stack.PushColor(ImGuiCol.ResizeGrip, ResizeGripRgba); - stack.PushColor(ImGuiCol.ResizeGripHovered, ResizeGripHoveredRgba); - stack.PushColor(ImGuiCol.ResizeGripActive, ResizeGripActiveRgba); + // Resize grip + stack.PushColor(ImGuiCol.ResizeGrip, c.FrameBg); + stack.PushColor(ImGuiCol.ResizeGripHovered, c.AccentLight); + stack.PushColor(ImGuiCol.ResizeGripActive, c.Accent); - // Check mark + slider grab — primary cyan. - stack.PushColor(ImGuiCol.CheckMark, PrimaryRgba); - stack.PushColor(ImGuiCol.SliderGrab, PrimaryRgba); - stack.PushColor(ImGuiCol.SliderGrabActive, PrimaryHoverRgba); + // Check mark + slider grab + stack.PushColor(ImGuiCol.CheckMark, c.Primary); + stack.PushColor(ImGuiCol.SliderGrab, c.Primary); + stack.PushColor(ImGuiCol.SliderGrabActive, c.PrimaryLight); - // Separator — primary cyan when hovered/active so the eye - // immediately sees that splitters are interactive. - stack.PushColor(ImGuiCol.Separator, BorderRgba); - stack.PushColor(ImGuiCol.SeparatorHovered, PrimaryHoverRgba); - stack.PushColor(ImGuiCol.SeparatorActive, PrimaryRgba); + // Separator + stack.PushColor(ImGuiCol.Separator, c.Border); + stack.PushColor(ImGuiCol.SeparatorHovered, c.PrimaryLight); + stack.PushColor(ImGuiCol.SeparatorActive, c.Primary); return stack; } diff --git a/HellionChat/Ui/Popout.cs b/HellionChat/Ui/Popout.cs index a3944ff..f86c980 100644 --- a/HellionChat/Ui/Popout.cs +++ b/HellionChat/Ui/Popout.cs @@ -103,9 +103,7 @@ internal class Popout : Window } else { - BgAlpha = Plugin.Config.HellionThemeEnabled - ? Plugin.Config.HellionThemeWindowOpacity - : Plugin.Config.WindowAlpha / 100f; + BgAlpha = Plugin.Config.WindowOpacity; } } } diff --git a/HellionChat/Ui/SettingsTabs/Appearance.cs b/HellionChat/Ui/SettingsTabs/Appearance.cs index 6517f74..ce09f4c 100644 --- a/HellionChat/Ui/SettingsTabs/Appearance.cs +++ b/HellionChat/Ui/SettingsTabs/Appearance.cs @@ -45,6 +45,13 @@ internal sealed class Appearance : ISettingsTab using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) { + // v1.1.0 — Diese Settings-UI wird in Phase J durch den dedizierten + // Themes-Tab ersetzt. Bis dahin bleiben die alten Toggles erhalten, + // damit die Settings-Seite kompiliert; sie schreiben in die mit + // [Obsolete] markierten Felder, die bis v1.2.0 als JSON-Safety-Net + // bestehen bleiben. Das pragma unterdrückt die CS0612-Warnungen + // gezielt für diesen Übergangs-Block. +#pragma warning disable CS0612, CS0618 ImGui.Checkbox(HellionStrings.Theme_Enabled_Name, ref Mutable.HellionThemeEnabled); ImGuiUtil.HelpMarker(HellionStrings.Theme_Enabled_Description); @@ -81,6 +88,7 @@ internal sealed class Appearance : ISettingsTab { ImGuiUtil.DragFloatVertical(Language.Options_WindowOpacity_Name, ref Mutable.WindowAlpha, .25f, 0f, 100f, $"{Mutable.WindowAlpha:N2}%%", ImGuiSliderFlags.AlwaysClamp); } +#pragma warning restore CS0612, CS0618 } } -- 2.52.0 From cb5c940a84e9f5ef2c29e0042f8bc8b182298b38 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 14:02:13 +0200 Subject: [PATCH 039/169] feat(settings): card-grid overview router --- HellionChat/Ui/Settings.cs | 42 +++++++++++++- HellionChat/Ui/SettingsOverview.cs | 90 ++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 HellionChat/Ui/SettingsOverview.cs diff --git a/HellionChat/Ui/Settings.cs b/HellionChat/Ui/Settings.cs index 6abf6a6..538fc9f 100755 --- a/HellionChat/Ui/Settings.cs +++ b/HellionChat/Ui/Settings.cs @@ -9,13 +9,21 @@ using Dalamud.Bindings.ImGui; namespace HellionChat.Ui; +internal enum SettingsView +{ + Overview, + Detail, +} + public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window { - private readonly Plugin Plugin; + internal readonly Plugin Plugin; private Configuration Mutable { get; } private List Tabs { get; } private int CurrentTab; + private SettingsView View = SettingsView.Overview; + private readonly SettingsOverview Overview; internal SettingsWindow(Plugin plugin) : base($"{Language.Settings_Title.Format(Plugin.PluginName)}###chat2-settings") { @@ -31,6 +39,8 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window Plugin = plugin; Mutable = new Configuration(); + Overview = new SettingsOverview(this); + Tabs = [ new General(Plugin, Mutable), @@ -72,8 +82,33 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window public override void Draw() { if (ImGui.IsWindowAppearing()) + { Initialise(); + View = SettingsView.Overview; + } + if (View == SettingsView.Overview) + Overview.Draw(); + else + DrawDetail(); + + ImGui.Separator(); + DrawSaveButtons(); + } + + internal void OpenSection(int tabIndex) + { + CurrentTab = tabIndex; + View = SettingsView.Detail; + } + + internal void OpenOverview() + { + View = SettingsView.Overview; + } + + private void DrawDetail() + { using (var table = ImRaii.Table("##chat2-settings-table", 2)) { if (table.Success) @@ -103,9 +138,10 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window Tabs[CurrentTab].Draw(changed); } } + } - ImGui.Separator(); - + private void DrawSaveButtons() + { var save = ImGui.Button(Language.Settings_Save); ImGui.SameLine(); diff --git a/HellionChat/Ui/SettingsOverview.cs b/HellionChat/Ui/SettingsOverview.cs new file mode 100644 index 0000000..0574021 --- /dev/null +++ b/HellionChat/Ui/SettingsOverview.cs @@ -0,0 +1,90 @@ +using System.Numerics; +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; +using HellionChat.Resources; +using HellionChat.Util; + +namespace HellionChat.Ui; + +internal sealed class SettingsOverview +{ + private readonly SettingsWindow _window; + + // Card-Reihenfolge entspricht 1:1 dem Tabs-Index in SettingsWindow. + // Themes (Phase J) wird später als Card 2 zwischen Appearance und Window + // eingeschoben — dabei muss diese Liste neu gemappt werden. + private static readonly (FontAwesomeIcon Icon, string TitleKey, string SubtextKey)[] CardDefs = + [ + (FontAwesomeIcon.SlidersH, "Settings_Card_General_Title", "Settings_Card_General_Subtext"), + (FontAwesomeIcon.Palette, "Settings_Card_Appearance_Title", "Settings_Card_Appearance_Subtext"), + (FontAwesomeIcon.WindowMaximize, "Settings_Card_Window_Title", "Settings_Card_Window_Subtext"), + (FontAwesomeIcon.Comments, "Settings_Card_Chat_Title", "Settings_Card_Chat_Subtext"), + (FontAwesomeIcon.FolderTree, "Settings_Card_Tabs_Title", "Settings_Card_Tabs_Subtext"), + (FontAwesomeIcon.ShieldAlt, "Settings_Card_Privacy_Title", "Settings_Card_Privacy_Subtext"), + (FontAwesomeIcon.Database, "Settings_Card_Database_Title", "Settings_Card_Database_Subtext"), + (FontAwesomeIcon.InfoCircle, "Settings_Card_Information_Title", "Settings_Card_Information_Subtext"), + ]; + + public SettingsOverview(SettingsWindow window) + { + _window = window; + } + + public void Draw() + { + var avail = ImGui.GetContentRegionAvail(); + var columns = avail.X >= 700f ? 3 : 2; + var cardWidth = (avail.X - (columns - 1) * 8f) / columns; + var cardHeight = 96f; + + for (var i = 0; i < CardDefs.Length; i++) + { + var (icon, titleKey, subtextKey) = CardDefs[i]; + var title = HellionStrings.ResourceManager.GetString(titleKey) ?? titleKey; + var subtext = HellionStrings.ResourceManager.GetString(subtextKey) ?? subtextKey; + DrawCard(i, icon, title, subtext, cardWidth, cardHeight); + + if ((i + 1) % columns != 0 && i != CardDefs.Length - 1) + ImGui.SameLine(); + } + } + + private void DrawCard(int index, FontAwesomeIcon icon, string title, string subtext, float w, float h) + { + var cursorBefore = ImGui.GetCursorScreenPos(); + var clicked = ImGui.InvisibleButton($"##settings-card-{index}", new Vector2(w, h)); + var hovered = ImGui.IsItemHovered(); + var bgColor = hovered ? 0xFF22303Fu : 0xFF1A2538u; + + var draw = ImGui.GetWindowDrawList(); + draw.AddRectFilled(cursorBefore, cursorBefore + new Vector2(w, h), bgColor, 4f); + + var textPos = cursorBefore + new Vector2(16f, 12f); + ImGui.SetCursorScreenPos(textPos); + // Plugin ist hier Instanz, nicht Static-Type — daher über _window + // referenzieren (Codebase-Konvention, siehe ImGuiUtil.cs:22 für die + // alternative Static-Init-Pattern, das wir hier nicht nutzen). + using (_window.Plugin.FontManager.FontAwesome.Push()) + { + ImGui.Text(icon.ToIconString()); + } + + ImGui.SetCursorScreenPos(textPos + new Vector2(0f, 28f)); + ImGui.TextUnformatted(title); + + ImGui.SetCursorScreenPos(textPos + new Vector2(0f, 50f)); + using (ImRaii.PushColor(ImGuiCol.Text, 0xFF8FA3B5u)) + { + ImGui.TextUnformatted(subtext); + } + + // Cursor unter die Card setzen für nächsten Item-Pass + ImGui.SetCursorScreenPos(cursorBefore + new Vector2(0f, h + 8f)); + + if (clicked) + { + _window.OpenSection(index); + } + } +} -- 2.52.0 From c878d24d11cb89c694d7c0b09ae07974b49d3be2 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 14:05:59 +0200 Subject: [PATCH 040/169] feat(themes): settings tab with built-in and custom theme grids --- HellionChat/Ui/Settings.cs | 1 + HellionChat/Ui/SettingsOverview.cs | 4 +- HellionChat/Ui/SettingsTabs/Themes.cs | 147 ++++++++++++++++++++++++++ 3 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 HellionChat/Ui/SettingsTabs/Themes.cs diff --git a/HellionChat/Ui/Settings.cs b/HellionChat/Ui/Settings.cs index 538fc9f..cec9ea5 100755 --- a/HellionChat/Ui/Settings.cs +++ b/HellionChat/Ui/Settings.cs @@ -45,6 +45,7 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window [ new General(Plugin, Mutable), new Appearance(Plugin, Mutable), + new SettingsTabs.Themes(Plugin, Mutable), new SettingsTabs.Window(Plugin, Mutable), new Chat(Plugin, Mutable), new SettingsTabs.Tabs(Plugin, Mutable), diff --git a/HellionChat/Ui/SettingsOverview.cs b/HellionChat/Ui/SettingsOverview.cs index 0574021..9715bbe 100644 --- a/HellionChat/Ui/SettingsOverview.cs +++ b/HellionChat/Ui/SettingsOverview.cs @@ -12,12 +12,12 @@ internal sealed class SettingsOverview private readonly SettingsWindow _window; // Card-Reihenfolge entspricht 1:1 dem Tabs-Index in SettingsWindow. - // Themes (Phase J) wird später als Card 2 zwischen Appearance und Window - // eingeschoben — dabei muss diese Liste neu gemappt werden. + // Themes ist Card-Index 2, eingeschoben zwischen Appearance und Window. private static readonly (FontAwesomeIcon Icon, string TitleKey, string SubtextKey)[] CardDefs = [ (FontAwesomeIcon.SlidersH, "Settings_Card_General_Title", "Settings_Card_General_Subtext"), (FontAwesomeIcon.Palette, "Settings_Card_Appearance_Title", "Settings_Card_Appearance_Subtext"), + (FontAwesomeIcon.Swatchbook, "Settings_Card_Themes_Title", "Settings_Card_Themes_Subtext"), (FontAwesomeIcon.WindowMaximize, "Settings_Card_Window_Title", "Settings_Card_Window_Subtext"), (FontAwesomeIcon.Comments, "Settings_Card_Chat_Title", "Settings_Card_Chat_Subtext"), (FontAwesomeIcon.FolderTree, "Settings_Card_Tabs_Title", "Settings_Card_Tabs_Subtext"), diff --git a/HellionChat/Ui/SettingsTabs/Themes.cs b/HellionChat/Ui/SettingsTabs/Themes.cs new file mode 100644 index 0000000..0fe85af --- /dev/null +++ b/HellionChat/Ui/SettingsTabs/Themes.cs @@ -0,0 +1,147 @@ +using System.Numerics; +using Dalamud.Bindings.ImGui; +using Dalamud.Interface.Utility.Raii; +using HellionChat.Resources; +using HellionChat.Themes; +using HellionChat.Util; + +namespace HellionChat.Ui.SettingsTabs; + +internal sealed class Themes : ISettingsTab +{ + private readonly Plugin Plugin; + private readonly Configuration Mutable; + + public string Name => HellionStrings.ResourceManager.GetString("Settings_Tab_Themes") ?? "Themes" + "###tabs-themes"; + + internal Themes(Plugin plugin, Configuration mutable) + { + Plugin = plugin; + Mutable = mutable; + } + + public void Draw(bool changed) + { + var registry = Plugin.ThemeRegistry; + var active = registry.Get(Mutable.Theme); + + var activeLabelTemplate = HellionStrings.ResourceManager.GetString("Settings_Themes_Active") ?? "Active: {0}"; + ImGui.TextUnformatted(string.Format(activeLabelTemplate, active.Name)); + using (ImRaii.PushColor(ImGuiCol.Text, 0xFF8FA3B5u)) + ImGui.TextUnformatted(active.Author); + + ImGui.Spacing(); + ImGui.Separator(); + ImGui.Spacing(); + + var builtInsLabel = HellionStrings.ResourceManager.GetString("Settings_Themes_BuiltIns") ?? "Built-in themes"; + ImGui.TextUnformatted(builtInsLabel); + ImGui.Spacing(); + DrawThemeGrid(registry.AllBuiltIns(), active.Slug); + + var customs = registry.AllCustom().ToList(); + if (customs.Count > 0) + { + ImGui.Spacing(); + ImGui.Separator(); + ImGui.Spacing(); + var customLabel = HellionStrings.ResourceManager.GetString("Settings_Themes_Custom") ?? "Custom themes"; + ImGui.TextUnformatted(customLabel); + ImGui.Spacing(); + DrawThemeGrid(customs, active.Slug); + } + + ImGui.Spacing(); + ImGui.Separator(); + ImGui.Spacing(); + + var openFolderLabel = HellionStrings.ResourceManager.GetString("Settings_Themes_OpenFolder") ?? "Open themes folder"; + if (ImGui.Button(openFolderLabel)) + { + var dir = Path.Combine(Plugin.Interface.ConfigDirectory.FullName, "themes"); + Directory.CreateDirectory(dir); + Dalamud.Utility.Util.OpenLink(dir); + } + + ImGui.SameLine(); + var exportLabel = HellionStrings.ResourceManager.GetString("Settings_Themes_ExportActive") ?? "Export active..."; + if (ImGui.Button(exportLabel)) + { + // Export-Logik wird in Phase L (Task 21) ergänzt — Stub belassen, Button bleibt sichtbar. + } + } + + private void DrawThemeGrid(IEnumerable themes, string activeSlug) + { + var avail = ImGui.GetContentRegionAvail(); + var columns = avail.X >= 700f ? 3 : 2; + var cardWidth = (avail.X - (columns - 1) * 8f) / columns; + var cardHeight = 110f; + var i = 0; + foreach (var theme in themes) + { + DrawThemeCard(theme, activeSlug, cardWidth, cardHeight); + i++; + if (i % columns != 0) + ImGui.SameLine(); + else + ImGui.NewLine(); + } + } + + private void DrawThemeCard(Theme theme, string activeSlug, float w, float h) + { + var isActive = string.Equals(theme.Slug, activeSlug, StringComparison.OrdinalIgnoreCase); + var cursorBefore = ImGui.GetCursorScreenPos(); + var clicked = ImGui.InvisibleButton($"##theme-card-{theme.Slug}", new Vector2(w, h)); + var hovered = ImGui.IsItemHovered(); + + var draw = ImGui.GetWindowDrawList(); + var bg = ColourUtil.RgbaToAbgr(theme.Colors.WindowBg | 0xFFu); + draw.AddRectFilled(cursorBefore, cursorBefore + new Vector2(w, h), bg, 4f); + + if (isActive) + { + var border = ColourUtil.RgbaToAbgr(theme.Colors.Primary); + draw.AddRect(cursorBefore, cursorBefore + new Vector2(w, h), border, 4f, ImDrawFlags.None, 2f); + } + else if (hovered) + { + var border = ColourUtil.RgbaToAbgr(theme.Colors.PrimaryLight & 0xFFFFFF99u); + draw.AddRect(cursorBefore, cursorBefore + new Vector2(w, h), border, 4f, ImDrawFlags.None, 1f); + } + + // Akzent-Swatch links oben + var swatchPos = cursorBefore + new Vector2(12f, 12f); + var swatchSize = new Vector2(20f, 20f); + draw.AddRectFilled(swatchPos, swatchPos + swatchSize, ColourUtil.RgbaToAbgr(theme.Colors.Primary), 3f); + + // Name + ImGui.SetCursorScreenPos(cursorBefore + new Vector2(40f, 12f)); + var textColor = ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary); + using (ImRaii.PushColor(ImGuiCol.Text, textColor)) + ImGui.TextUnformatted(theme.Name); + + // Author + ImGui.SetCursorScreenPos(cursorBefore + new Vector2(40f, 32f)); + var mutedColor = ColourUtil.RgbaToAbgr(theme.Colors.TextMuted); + using (ImRaii.PushColor(ImGuiCol.Text, mutedColor)) + ImGui.TextUnformatted(theme.Author); + + // Description (wrapped, falls zu lang) + ImGui.SetCursorScreenPos(cursorBefore + new Vector2(12f, 60f)); + ImGui.PushTextWrapPos(cursorBefore.X + w - 12f); + using (ImRaii.PushColor(ImGuiCol.Text, mutedColor)) + ImGui.TextUnformatted(theme.Description); + ImGui.PopTextWrapPos(); + + // Cursor unter die Card setzen + ImGui.SetCursorScreenPos(cursorBefore + new Vector2(0f, h + 8f)); + + if (clicked) + { + Mutable.Theme = theme.Slug; + Plugin.ThemeRegistry.Switch(theme.Slug); + } + } +} -- 2.52.0 From 485dc4e1b4dbbed225d74c73f2589a5257af28d5 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 14:09:02 +0200 Subject: [PATCH 041/169] i18n(themes): localize theme settings card grid (en/de) --- .../Resources/HellionStrings.Designer.cs | 28 ++++++++ HellionChat/Resources/HellionStrings.de.resx | 72 +++++++++++++++++++ HellionChat/Resources/HellionStrings.resx | 72 +++++++++++++++++++ 3 files changed, 172 insertions(+) diff --git a/HellionChat/Resources/HellionStrings.Designer.cs b/HellionChat/Resources/HellionStrings.Designer.cs index fb80313..deb2ea1 100644 --- a/HellionChat/Resources/HellionStrings.Designer.cs +++ b/HellionChat/Resources/HellionStrings.Designer.cs @@ -203,6 +203,34 @@ internal class HellionStrings internal static string Settings_Tab_Database => Get(nameof(Settings_Tab_Database)); internal static string Settings_Tab_Information => Get(nameof(Settings_Tab_Information)); + // v1.1.0 — Settings card-grid overview + internal static string Settings_Card_General_Title => Get(nameof(Settings_Card_General_Title)); + internal static string Settings_Card_General_Subtext => Get(nameof(Settings_Card_General_Subtext)); + internal static string Settings_Card_Appearance_Title => Get(nameof(Settings_Card_Appearance_Title)); + internal static string Settings_Card_Appearance_Subtext => Get(nameof(Settings_Card_Appearance_Subtext)); + internal static string Settings_Card_Themes_Title => Get(nameof(Settings_Card_Themes_Title)); + internal static string Settings_Card_Themes_Subtext => Get(nameof(Settings_Card_Themes_Subtext)); + internal static string Settings_Card_Window_Title => Get(nameof(Settings_Card_Window_Title)); + internal static string Settings_Card_Window_Subtext => Get(nameof(Settings_Card_Window_Subtext)); + internal static string Settings_Card_Chat_Title => Get(nameof(Settings_Card_Chat_Title)); + internal static string Settings_Card_Chat_Subtext => Get(nameof(Settings_Card_Chat_Subtext)); + internal static string Settings_Card_Tabs_Title => Get(nameof(Settings_Card_Tabs_Title)); + internal static string Settings_Card_Tabs_Subtext => Get(nameof(Settings_Card_Tabs_Subtext)); + internal static string Settings_Card_Privacy_Title => Get(nameof(Settings_Card_Privacy_Title)); + internal static string Settings_Card_Privacy_Subtext => Get(nameof(Settings_Card_Privacy_Subtext)); + internal static string Settings_Card_Database_Title => Get(nameof(Settings_Card_Database_Title)); + internal static string Settings_Card_Database_Subtext => Get(nameof(Settings_Card_Database_Subtext)); + internal static string Settings_Card_Information_Title => Get(nameof(Settings_Card_Information_Title)); + internal static string Settings_Card_Information_Subtext => Get(nameof(Settings_Card_Information_Subtext)); + + // v1.1.0 — Themes-Settings-Tab + internal static string Settings_Tab_Themes => Get(nameof(Settings_Tab_Themes)); + internal static string Settings_Themes_Active => Get(nameof(Settings_Themes_Active)); + internal static string Settings_Themes_BuiltIns => Get(nameof(Settings_Themes_BuiltIns)); + internal static string Settings_Themes_Custom => Get(nameof(Settings_Themes_Custom)); + internal static string Settings_Themes_OpenFolder => Get(nameof(Settings_Themes_OpenFolder)); + internal static string Settings_Themes_ExportActive => Get(nameof(Settings_Themes_ExportActive)); + // Hellion Chat — General-Tab section headings internal static string Settings_General_Input_Heading => Get(nameof(Settings_General_Input_Heading)); internal static string Settings_General_Audio_Heading => Get(nameof(Settings_General_Audio_Heading)); diff --git a/HellionChat/Resources/HellionStrings.de.resx b/HellionChat/Resources/HellionStrings.de.resx index 9f88ea0..207bb35 100644 --- a/HellionChat/Resources/HellionStrings.de.resx +++ b/HellionChat/Resources/HellionStrings.de.resx @@ -624,4 +624,76 @@ Chat 2 in /xlplugins deaktivieren, danach Hellion Chat erneut aktivieren. + + Allgemein + + + Sprache und grundlegendes Verhalten + + + Erscheinungsbild + + + Fensterdeckkraft, Schriften, Bewegung + + + Themes + + + Theme wählen oder eigenes importieren + + + Fenster + + + Fensterposition, Rahmen, Hide-Zustände + + + Chat + + + Chat-Verhalten, Emotes, Auto-Tells + + + Tabs + + + Tab-Layout, Kanäle, eigene Tabs + + + Datenschutz + + + Filter, Aufbewahrung, Bereinigung, Export + + + Datenbank + + + Speicher, Migration, alte Bereinigung + + + Information + + + Über, Mitwirkende, Support + + + Themes + + + Aktiv: {0} + + + Eingebaute Themes + + + Eigene Themes + + + Themes-Ordner öffnen + + + Aktives exportieren... + diff --git a/HellionChat/Resources/HellionStrings.resx b/HellionChat/Resources/HellionStrings.resx index aefa6c6..b34833f 100644 --- a/HellionChat/Resources/HellionStrings.resx +++ b/HellionChat/Resources/HellionStrings.resx @@ -624,4 +624,76 @@ Disable Chat 2 in /xlplugins, then re-enable Hellion Chat. + + General + + + Language and basic behaviour + + + Appearance + + + Window opacity, fonts, motion + + + Themes + + + Choose a theme or import your own + + + Window + + + Window position, frame, hide states + + + Chat + + + Chat behaviour, emotes, auto-tells + + + Tabs + + + Tab layout, channels, custom tabs + + + Privacy + + + Filter, retention, cleanup, export + + + Database + + + Storage, migration, legacy cleanup + + + Information + + + About, credits, support + + + Themes + + + Active: {0} + + + Built-in themes + + + Custom themes + + + Open themes folder + + + Export active... + -- 2.52.0 From af4651b37eb31d8b610ae5875f6ae3047b7e1319 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 14:11:14 +0200 Subject: [PATCH 042/169] feat(themes): export active theme to json --- HellionChat/Themes/ThemeJsonWriter.cs | 65 +++++++++++++++++++++++++++ HellionChat/Ui/SettingsTabs/Themes.cs | 8 +++- 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 HellionChat/Themes/ThemeJsonWriter.cs diff --git a/HellionChat/Themes/ThemeJsonWriter.cs b/HellionChat/Themes/ThemeJsonWriter.cs new file mode 100644 index 0000000..97523f1 --- /dev/null +++ b/HellionChat/Themes/ThemeJsonWriter.cs @@ -0,0 +1,65 @@ +using System.Text.Json; + +namespace HellionChat.Themes; + +internal static class ThemeJsonWriter +{ + public static string Serialize(Theme theme) + { + using var ms = new MemoryStream(); + using (var writer = new Utf8JsonWriter(ms, new JsonWriterOptions { Indented = true })) + { + writer.WriteStartObject(); + writer.WriteNumber("schemaVersion", ThemeJsonLoader.SupportedSchemaVersion); + writer.WriteString("slug", theme.Slug); + writer.WriteString("name", theme.Name); + writer.WriteString("author", theme.Author); + writer.WriteString("description", theme.Description); + + writer.WriteStartObject("colors"); + WriteColor(writer, "primaryDark", theme.Colors.PrimaryDark); + WriteColor(writer, "primary", theme.Colors.Primary); + WriteColor(writer, "primaryLight", theme.Colors.PrimaryLight); + WriteColor(writer, "primaryGlow", theme.Colors.PrimaryGlow); + WriteColor(writer, "accentDark", theme.Colors.AccentDark); + WriteColor(writer, "accent", theme.Colors.Accent); + WriteColor(writer, "accentLight", theme.Colors.AccentLight); + WriteColor(writer, "identity", theme.Colors.Identity); + WriteColor(writer, "windowBg", theme.Colors.WindowBg); + WriteColor(writer, "childBg", theme.Colors.ChildBg); + WriteColor(writer, "frameBg", theme.Colors.FrameBg); + WriteColor(writer, "surface", theme.Colors.Surface); + WriteColor(writer, "surfaceHover", theme.Colors.SurfaceHover); + WriteColor(writer, "border", theme.Colors.Border); + WriteColor(writer, "textPrimary", theme.Colors.TextPrimary); + WriteColor(writer, "textMuted", theme.Colors.TextMuted); + WriteColor(writer, "textDim", theme.Colors.TextDim); + WriteColor(writer, "statusSuccess", theme.Colors.StatusSuccess); + WriteColor(writer, "statusDanger", theme.Colors.StatusDanger); + WriteColor(writer, "statusWarning", theme.Colors.StatusWarning); + WriteColor(writer, "statusInfo", theme.Colors.StatusInfo); + writer.WriteEndObject(); + + writer.WriteStartObject("layout"); + writer.WriteNumber("windowRounding", theme.Layout.WindowRounding); + writer.WriteNumber("childRounding", theme.Layout.ChildRounding); + writer.WriteNumber("popupRounding", theme.Layout.PopupRounding); + writer.WriteNumber("frameRounding", theme.Layout.FrameRounding); + writer.WriteNumber("grabRounding", theme.Layout.GrabRounding); + writer.WriteNumber("tabRounding", theme.Layout.TabRounding); + writer.WriteNumber("scrollbarRounding", theme.Layout.ScrollbarRounding); + writer.WriteNumber("windowBorderSize", theme.Layout.WindowBorderSize); + writer.WriteNumber("frameBorderSize", theme.Layout.FrameBorderSize); + writer.WriteEndObject(); + + writer.WriteEndObject(); + } + + return System.Text.Encoding.UTF8.GetString(ms.ToArray()); + } + + private static void WriteColor(Utf8JsonWriter writer, string key, uint rgba) + { + writer.WriteString(key, $"#{rgba:X8}"); + } +} diff --git a/HellionChat/Ui/SettingsTabs/Themes.cs b/HellionChat/Ui/SettingsTabs/Themes.cs index 0fe85af..f04a5de 100644 --- a/HellionChat/Ui/SettingsTabs/Themes.cs +++ b/HellionChat/Ui/SettingsTabs/Themes.cs @@ -67,7 +67,13 @@ internal sealed class Themes : ISettingsTab var exportLabel = HellionStrings.ResourceManager.GetString("Settings_Themes_ExportActive") ?? "Export active..."; if (ImGui.Button(exportLabel)) { - // Export-Logik wird in Phase L (Task 21) ergänzt — Stub belassen, Button bleibt sichtbar. + var dir = Path.Combine(Plugin.Interface.ConfigDirectory.FullName, "themes"); + Directory.CreateDirectory(dir); + var fileName = $"{active.Slug}.export.json"; + var path = Path.Combine(dir, fileName); + var json = ThemeJsonWriter.Serialize(active); + File.WriteAllText(path, json); + Plugin.Log.Information($"Exported active theme '{active.Slug}' to {path}"); } } -- 2.52.0 From 8f9c01d3226ec30197d3de8600fc03b9ebd8520f Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 14:13:19 +0200 Subject: [PATCH 043/169] feat(themes): mini-mockup preview in theme cards --- HellionChat/Ui/SettingsTabs/ThemeMockup.cs | 70 ++++++++++++++++++++++ HellionChat/Ui/SettingsTabs/Themes.cs | 25 +++----- 2 files changed, 79 insertions(+), 16 deletions(-) create mode 100644 HellionChat/Ui/SettingsTabs/ThemeMockup.cs diff --git a/HellionChat/Ui/SettingsTabs/ThemeMockup.cs b/HellionChat/Ui/SettingsTabs/ThemeMockup.cs new file mode 100644 index 0000000..3fcb382 --- /dev/null +++ b/HellionChat/Ui/SettingsTabs/ThemeMockup.cs @@ -0,0 +1,70 @@ +using System.Numerics; +using Dalamud.Bindings.ImGui; +using HellionChat.Themes; +using HellionChat.Util; + +namespace HellionChat.Ui.SettingsTabs; + +internal static class ThemeMockup +{ + // Zeichnet ein Mini-Chat-Window-Mockup mit den Theme-Werten direkt + // ins WindowDrawList. Keine Texture, keine Allocation pro Frame — + // alles via DrawList.AddRectFilled / AddText. + public static void Draw(Vector2 origin, Vector2 size, Theme theme) + { + var draw = ImGui.GetWindowDrawList(); + var c = theme.Colors; + + // Window-Bg + draw.AddRectFilled(origin, origin + size, ColourUtil.RgbaToAbgr(c.WindowBg | 0xFFu), theme.Layout.WindowRounding); + + // Title-Bar + var titleHeight = 14f; + draw.AddRectFilled( + origin, + new Vector2(origin.X + size.X, origin.Y + titleHeight), + ColourUtil.RgbaToAbgr(c.Identity), theme.Layout.WindowRounding); + + // Tab-Bar — 3 Mini-Tabs + var tabY = origin.Y + titleHeight + 4f; + var tabHeight = 12f; + for (var i = 0; i < 3; i++) + { + var tabX = origin.X + 6f + i * 28f; + var color = i == 0 ? c.FrameBg : c.ChildBg; + draw.AddRectFilled( + new Vector2(tabX, tabY), + new Vector2(tabX + 26f, tabY + tabHeight), + ColourUtil.RgbaToAbgr(color), theme.Layout.TabRounding); + + if (i == 0) // Active-Pill + { + draw.AddRectFilled( + new Vector2(tabX, tabY + tabHeight - 2f), + new Vector2(tabX + 26f, tabY + tabHeight), + ColourUtil.RgbaToAbgr(c.Primary)); + } + } + + // Card-Row mit Mock-Sender + Text + var rowY = tabY + tabHeight + 6f; + var rowHeight = 18f; + draw.AddRectFilled( + new Vector2(origin.X + 6f, rowY), + new Vector2(origin.X + size.X - 6f, rowY + rowHeight), + ColourUtil.RgbaToAbgr(c.Surface), 2f); + + // Akzent-Button rechts unten + var btnW = 28f; + var btnH = 10f; + var btnX = origin.X + size.X - btnW - 6f; + var btnY = origin.Y + size.Y - btnH - 6f; + draw.AddRectFilled( + new Vector2(btnX, btnY), + new Vector2(btnX + btnW, btnY + btnH), + ColourUtil.RgbaToAbgr(c.Accent), theme.Layout.FrameRounding); + + // Border um das gesamte Mockup + draw.AddRect(origin, origin + size, ColourUtil.RgbaToAbgr(c.Border), theme.Layout.WindowRounding); + } +} diff --git a/HellionChat/Ui/SettingsTabs/Themes.cs b/HellionChat/Ui/SettingsTabs/Themes.cs index f04a5de..f501b6b 100644 --- a/HellionChat/Ui/SettingsTabs/Themes.cs +++ b/HellionChat/Ui/SettingsTabs/Themes.cs @@ -82,7 +82,7 @@ internal sealed class Themes : ISettingsTab var avail = ImGui.GetContentRegionAvail(); var columns = avail.X >= 700f ? 3 : 2; var cardWidth = (avail.X - (columns - 1) * 8f) / columns; - var cardHeight = 110f; + var cardHeight = 140f; // war 110f — Mockup braucht mehr Platz var i = 0; foreach (var theme in themes) { @@ -117,30 +117,23 @@ internal sealed class Themes : ISettingsTab draw.AddRect(cursorBefore, cursorBefore + new Vector2(w, h), border, 4f, ImDrawFlags.None, 1f); } - // Akzent-Swatch links oben - var swatchPos = cursorBefore + new Vector2(12f, 12f); - var swatchSize = new Vector2(20f, 20f); - draw.AddRectFilled(swatchPos, swatchPos + swatchSize, ColourUtil.RgbaToAbgr(theme.Colors.Primary), 3f); + // Mini-Mockup statt Akzent-Swatch — visualisiert das Theme im Mini-Chat-Window + var mockupOrigin = cursorBefore + new Vector2(12f, 12f); + var mockupSize = new Vector2(w - 24f, 60f); + ThemeMockup.Draw(mockupOrigin, mockupSize, theme); - // Name - ImGui.SetCursorScreenPos(cursorBefore + new Vector2(40f, 12f)); + // Name unter dem Mockup + ImGui.SetCursorScreenPos(cursorBefore + new Vector2(12f, 78f)); var textColor = ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary); using (ImRaii.PushColor(ImGuiCol.Text, textColor)) ImGui.TextUnformatted(theme.Name); - // Author - ImGui.SetCursorScreenPos(cursorBefore + new Vector2(40f, 32f)); + // Author dahinter dezent + ImGui.SetCursorScreenPos(cursorBefore + new Vector2(12f, 96f)); var mutedColor = ColourUtil.RgbaToAbgr(theme.Colors.TextMuted); using (ImRaii.PushColor(ImGuiCol.Text, mutedColor)) ImGui.TextUnformatted(theme.Author); - // Description (wrapped, falls zu lang) - ImGui.SetCursorScreenPos(cursorBefore + new Vector2(12f, 60f)); - ImGui.PushTextWrapPos(cursorBefore.X + w - 12f); - using (ImRaii.PushColor(ImGuiCol.Text, mutedColor)) - ImGui.TextUnformatted(theme.Description); - ImGui.PopTextWrapPos(); - // Cursor unter die Card setzen ImGui.SetCursorScreenPos(cursorBefore + new Vector2(0f, h + 8f)); -- 2.52.0 From 9103bbb8928fb5954acaf5a5bada805a77c1dd28 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 14:15:12 +0200 Subject: [PATCH 044/169] feat(settings): breadcrumb header and esc to return to overview --- HellionChat/Ui/Settings.cs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/HellionChat/Ui/Settings.cs b/HellionChat/Ui/Settings.cs index cec9ea5..62cf690 100755 --- a/HellionChat/Ui/Settings.cs +++ b/HellionChat/Ui/Settings.cs @@ -88,6 +88,18 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window View = SettingsView.Overview; } + // ESC im Detail-View kehrt zur Overview zurück. Window-Focus-Check ist + // Pflicht — sonst triggert ESC auch wenn der User ein anderes Fenster + // fokussiert hat und ESC fürs Game-Menü drückt (Codebase-Pattern siehe + // Util/SearchSelector.cs:37). + if (View == SettingsView.Detail + && ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows) + && ImGui.IsKeyPressed(ImGuiKey.Escape)) + { + View = SettingsView.Overview; + return; + } + if (View == SettingsView.Overview) Overview.Draw(); else @@ -110,6 +122,28 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window private void DrawDetail() { + // Breadcrumb-Header — Akzent-Cyan, klickbar, führt zurück zur Overview + using (ImRaii.PushColor(ImGuiCol.Text, 0xFF00BED2u)) + using (ImRaii.PushColor(ImGuiCol.Button, 0u)) + using (ImRaii.PushColor(ImGuiCol.ButtonHovered, 0x33FFFFFFu)) + using (ImRaii.PushColor(ImGuiCol.ButtonActive, 0x55FFFFFFu)) + { + if (ImGui.SmallButton("← Settings")) + { + View = SettingsView.Overview; + return; + } + } + ImGui.SameLine(); + ImGui.TextUnformatted("·"); + ImGui.SameLine(); + ImGui.TextUnformatted(Tabs[CurrentTab].Name.Split("###")[0]); + + ImGui.Spacing(); + ImGui.Separator(); + ImGui.Spacing(); + + // Tab-Liste + Content (wie vorher) using (var table = ImRaii.Table("##chat2-settings-table", 2)) { if (table.Success) -- 2.52.0 From 2f52cbb7d4340e3e23a187971c6dd9a96b5dbc44 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 14:17:22 +0200 Subject: [PATCH 045/169] feat(themes): seed example custom theme on first start --- HellionChat/HellionChat.csproj | 3 ++ HellionChat/Plugin.cs | 34 +++++++++++++++ HellionChat/Themes/Builtin/example-theme.json | 41 +++++++++++++++++++ 3 files changed, 78 insertions(+) create mode 100644 HellionChat/Themes/Builtin/example-theme.json diff --git a/HellionChat/HellionChat.csproj b/HellionChat/HellionChat.csproj index 0d048e8..cfe8103 100644 --- a/HellionChat/HellionChat.csproj +++ b/HellionChat/HellionChat.csproj @@ -75,6 +75,9 @@ HellionFont-OFL.txt + + HellionChat.Themes.Builtin.example-theme.json + diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index e5e7770..6cb47f5 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -291,6 +291,8 @@ public sealed class Plugin : IDalamudPlugin // v1.1.0 — Theme-Engine init. Custom-Themes liegen in // pluginConfigs/HellionChat/themes/, lazy geladen beim ersten Get. var customThemesDir = Path.Combine(Interface.ConfigDirectory.FullName, "themes"); + Directory.CreateDirectory(customThemesDir); + SeedExampleThemeIfEmpty(customThemesDir); ThemeRegistry = new Themes.ThemeRegistry(customThemesDir); ThemeRegistry.Switch(Config.Theme); @@ -675,4 +677,36 @@ public sealed class Plugin : IDalamudPlugin public static bool InBattle => Condition[ConditionFlag.InCombat]; public static bool GposeActive => Condition[ConditionFlag.WatchingCutscene]; public static bool CutsceneActive => Condition[ConditionFlag.OccupiedInCutSceneEvent] || Condition[ConditionFlag.WatchingCutscene78]; + + // v1.1.0 — wenn der themes/-Ordner leer ist, schreiben wir die embedded + // example-theme.json als Vorlage rein. Bestehende User-Customs werden + // nicht angefasst (existing JSONs lassen den Block überspringen). + private static void SeedExampleThemeIfEmpty(string dir) + { + if (Directory.EnumerateFiles(dir, "*.json").Any()) + return; + + var examplePath = Path.Combine(dir, "example-theme.json"); + var resourceStream = typeof(Plugin).Assembly.GetManifestResourceStream("HellionChat.Themes.Builtin.example-theme.json"); + if (resourceStream is null) + { + Log.Warning("Themes example template not found in assembly resources; skipping seed."); + return; + } + + try + { + using var fileStream = File.Create(examplePath); + resourceStream.CopyTo(fileStream); + Log.Information($"Seeded example-theme.json into {dir}"); + } + catch (IOException ex) + { + Log.Warning(ex, "Failed to seed example-theme.json; user can create custom themes manually."); + } + finally + { + resourceStream.Dispose(); + } + } } diff --git a/HellionChat/Themes/Builtin/example-theme.json b/HellionChat/Themes/Builtin/example-theme.json new file mode 100644 index 0000000..1f547f7 --- /dev/null +++ b/HellionChat/Themes/Builtin/example-theme.json @@ -0,0 +1,41 @@ +{ + "schemaVersion": 1, + "slug": "example-custom", + "name": "Example Custom", + "author": "You", + "description": "Starting template — duplicate, rename, edit colors and reload.", + "colors": { + "primaryDark": "#0097A7", + "primary": "#00BED2", + "primaryLight": "#4DD9E8", + "primaryGlow": "#00BED299", + "accentDark": "#E85D04", + "accent": "#F97316", + "accentLight": "#FB923C", + "identity": "#0097A7", + "windowBg": "#070B12", + "childBg": "#0C1220", + "frameBg": "#141E30", + "surface": "#1A2538", + "surfaceHover": "#22303F", + "border": "#00BED266", + "textPrimary": "#E6F4F1", + "textMuted": "#8FA3B5", + "textDim": "#566273", + "statusSuccess": "#5CB85C", + "statusDanger": "#D9534F", + "statusWarning": "#F0AD4E", + "statusInfo": "#00BED2" + }, + "layout": { + "windowRounding": 4, + "childRounding": 3, + "popupRounding": 3, + "frameRounding": 2, + "grabRounding": 2, + "tabRounding": 2, + "scrollbarRounding": 2, + "windowBorderSize": 1, + "frameBorderSize": 1 + } +} -- 2.52.0 From abcd0847ef6fdb2d0f85e10abbbf5c28b8dced94 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 14:22:23 +0200 Subject: [PATCH 046/169] fix(settings): restore cursor after card draw to keep grid layout intact --- HellionChat/Ui/SettingsOverview.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/HellionChat/Ui/SettingsOverview.cs b/HellionChat/Ui/SettingsOverview.cs index 9715bbe..9837b53 100644 --- a/HellionChat/Ui/SettingsOverview.cs +++ b/HellionChat/Ui/SettingsOverview.cs @@ -60,6 +60,13 @@ internal sealed class SettingsOverview var draw = ImGui.GetWindowDrawList(); draw.AddRectFilled(cursorBefore, cursorBefore + new Vector2(w, h), bgColor, 4f); + // ImGui hat den Cursor nach dem InvisibleButton bereits korrekt + // weitergesetzt (für die Layout-Engine). Wir merken uns die Position, + // verschieben den Cursor temporär für die Inhalts-Beschriftung und + // restoren ihn am Ende — sonst kollidieren manuelle Cursor-Sets mit + // SameLine im Caller und die Cards stapeln sich diagonal. + var cursorAfterButton = ImGui.GetCursorScreenPos(); + var textPos = cursorBefore + new Vector2(16f, 12f); ImGui.SetCursorScreenPos(textPos); // Plugin ist hier Instanz, nicht Static-Type — daher über _window @@ -79,8 +86,9 @@ internal sealed class SettingsOverview ImGui.TextUnformatted(subtext); } - // Cursor unter die Card setzen für nächsten Item-Pass - ImGui.SetCursorScreenPos(cursorBefore + new Vector2(0f, h + 8f)); + // Restore cursor to post-button position, so SameLine + Wrap im Caller + // wieder mit dem ImGui-Layout-Flow arbeiten. + ImGui.SetCursorScreenPos(cursorAfterButton); if (clicked) { -- 2.52.0 From c943a2cff3b8612ac5f2b46931e9fa76822ca834 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 14:23:41 +0200 Subject: [PATCH 047/169] fix(themes): drop legacy StyleModel push from chat log and pop-out MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pre-engine StyleModel override in ChatLogWindow.PreDraw and Popout.PreDraw was layering an extra Dalamud style on top of the Hellion theme, locally tinting the chat window back to a non-Hellion look while every other plugin window rendered correctly. Theme is now the single source of truth — pick chat2-classic for the upstream flavour. --- HellionChat/Ui/ChatLogWindow.cs | 28 +++++----------------------- HellionChat/Ui/Popout.cs | 25 ++++--------------------- 2 files changed, 9 insertions(+), 44 deletions(-) diff --git a/HellionChat/Ui/ChatLogWindow.cs b/HellionChat/Ui/ChatLogWindow.cs index f5fda68..5949104 100644 --- a/HellionChat/Ui/ChatLogWindow.cs +++ b/HellionChat/Ui/ChatLogWindow.cs @@ -521,28 +521,16 @@ public sealed class ChatLogWindow : Window return FrameTime - lastActivityTime <= 1000 * Plugin.Config.InactivityHideTimeout; } - // Tracks the style instance pushed in PreDraw so PostDraw can pop the same - // one even if the user toggled OverrideStyle / ChosenStyle mid-frame. - // Without this, a config change between PreDraw and PostDraw could either - // leak a Push (no matching Pop) or pop nothing while we still have a frame - // pushed onto the ImGui stack. - private StyleModel? _pushedStyle; - public override void PreDraw() { if (Plugin.Config.KeepInputFocus && Activate) ImGui.SetWindowFocus(WindowName); - _pushedStyle = null; - if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null }) - { - var style = StyleModel.GetConfiguredStyles()?.FirstOrDefault(s => s.Name == Plugin.Config.ChosenStyle); - if (style != null) - { - style.Push(); - _pushedStyle = style; - } - } + // Hellion Chat v1.1.0+ — Theme-Engine ist Source-of-Truth, kein + // zusätzlicher Dalamud-StyleModel-Override mehr pro Window. Plugin.Draw + // pusht das aktive Hellion-Theme global; ChatLogWindow zeichnet sich + // damit konsistent zu Settings/Pop-Out/Wizard. Wer den Upstream-Look + // will, wählt das Built-In-Theme "Chat 2 Klassik" in Settings → Themes. } public override void PostDraw() @@ -553,12 +541,6 @@ public sealed class ChatLogWindow : Window // doesn't get called if the input is disabled. if (Plugin.CurrentTab.InputDisabled) Activate = false; - - if (_pushedStyle != null) - { - _pushedStyle.Pop(); - _pushedStyle = null; - } } public override void OnClose() diff --git a/HellionChat/Ui/Popout.cs b/HellionChat/Ui/Popout.cs index f86c980..dc10cd2 100644 --- a/HellionChat/Ui/Popout.cs +++ b/HellionChat/Ui/Popout.cs @@ -65,23 +65,12 @@ internal class Popout : Window return FrameTime - lastActivityTime <= 1000 * Plugin.Config.InactivityHideTimeout; } - // Tracks the style instance pushed in PreDraw so PostDraw pops the same - // one even if config changes mid-frame. See AUDIT-2026-05-05 [CR-UI-5]. - private StyleModel? _pushedStyle; - public override void PreDraw() { - _pushedStyle = null; - if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null }) - { - var style = StyleModel.GetConfiguredStyles()?.FirstOrDefault(s => s.Name == Plugin.Config.ChosenStyle); - if (style != null) - { - style.Push(); - _pushedStyle = style; - } - } - + // Hellion Chat v1.1.0+ — Theme-Engine ist Source-of-Truth, kein + // zusätzlicher Dalamud-StyleModel-Override mehr pro Window. Plugin.Draw + // pusht das aktive Hellion-Theme global; Pop-Out zeichnet sich damit + // konsistent zum Haupt-Chat-Window. Flags = ImGuiWindowFlags.None; if (!Plugin.Config.ShowPopOutTitleBar) Flags |= ImGuiWindowFlags.NoTitleBar; @@ -210,12 +199,6 @@ internal class Popout : Window { if (Idx >= 0 && Idx < ChatLogWindow.PopOutDocked.Count) ChatLogWindow.PopOutDocked[Idx] = ImGui.IsWindowDocked(); - - if (_pushedStyle != null) - { - _pushedStyle.Pop(); - _pushedStyle = null; - } } public override void OnClose() -- 2.52.0 From d41cea00314c34eec7108e424393b9d21b1e5066 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 14:28:24 +0200 Subject: [PATCH 048/169] fix(settings): card grid wraps correctly, detail view drops legacy tab list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SettingsOverview now wraps each card in BeginGroup/EndGroup so SameLine in the loop can wrap rows. The card content is drawn directly into the DrawList (icon, title, subtext) without cursor hopping that broke the flow. DrawDetail no longer renders the second-column tab list — the user has already picked a section from the overview, the redundant column made the detail view feel like the old vanilla settings layout. Section content now uses the full width. --- HellionChat/Ui/Settings.cs | 39 +++++++-------------------- HellionChat/Ui/SettingsOverview.cs | 43 ++++++++++++++---------------- 2 files changed, 30 insertions(+), 52 deletions(-) diff --git a/HellionChat/Ui/Settings.cs b/HellionChat/Ui/Settings.cs index 62cf690..2a61094 100755 --- a/HellionChat/Ui/Settings.cs +++ b/HellionChat/Ui/Settings.cs @@ -143,36 +143,17 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window ImGui.Separator(); ImGui.Spacing(); - // Tab-Liste + Content (wie vorher) - using (var table = ImRaii.Table("##chat2-settings-table", 2)) - { - if (table.Success) - { - ImGui.TableSetupColumn("tab", ImGuiTableColumnFlags.WidthFixed); - ImGui.TableSetupColumn("settings", ImGuiTableColumnFlags.WidthStretch); + // Section-Content in voller Breite. Die Tab-Liste links ist überholt: + // der User ist bereits über die Card-Übersicht navigiert, eine zweite + // Tab-Liste daneben würde nur den Vanilla-Look zurückbringen. Falls + // der User in eine andere Section will, geht er zurück zur Overview + // (Breadcrumb / ESC). + var style = ImGui.GetStyle(); + var height = ImGui.GetContentRegionAvail().Y - style.FramePadding.Y * 2 - style.ItemSpacing.Y - style.ItemInnerSpacing.Y * 2 - ImGui.CalcTextSize("A").Y; - ImGui.TableNextColumn(); - - var changed = false; - for (var i = 0; i < Tabs.Count; i++) - { - if (!ImGui.Selectable($"{Tabs[i].Name}###tab-{i}", CurrentTab == i)) - continue; - - CurrentTab = i; - changed = true; - } - - ImGui.TableNextColumn(); - - var style = ImGui.GetStyle(); - var height = ImGui.GetContentRegionAvail().Y - style.FramePadding.Y * 2 - style.ItemSpacing.Y - style.ItemInnerSpacing.Y * 2 - ImGui.CalcTextSize("A").Y; - - using var child = ImRaii.Child("##chat2-settings", new Vector2(-1, height)); - if (child.Success) - Tabs[CurrentTab].Draw(changed); - } - } + using var child = ImRaii.Child("##chat2-settings-detail", new Vector2(-1, height)); + if (child.Success) + Tabs[CurrentTab].Draw(false); } private void DrawSaveButtons() diff --git a/HellionChat/Ui/SettingsOverview.cs b/HellionChat/Ui/SettingsOverview.cs index 9837b53..b0f8a96 100644 --- a/HellionChat/Ui/SettingsOverview.cs +++ b/HellionChat/Ui/SettingsOverview.cs @@ -52,6 +52,11 @@ internal sealed class SettingsOverview private void DrawCard(int index, FontAwesomeIcon icon, string title, string subtext, float w, float h) { + // BeginGroup macht den Card-Bereich zu einem einzelnen ImGui-Layout-Item. + // Damit funktioniert SameLine() im Caller-Loop — sonst tracked ImGui die + // einzelnen InvisibleButton/Text-Items separat und das Wrapping bricht. + ImGui.BeginGroup(); + var cursorBefore = ImGui.GetCursorScreenPos(); var clicked = ImGui.InvisibleButton($"##settings-card-{index}", new Vector2(w, h)); var hovered = ImGui.IsItemHovered(); @@ -60,35 +65,27 @@ internal sealed class SettingsOverview var draw = ImGui.GetWindowDrawList(); draw.AddRectFilled(cursorBefore, cursorBefore + new Vector2(w, h), bgColor, 4f); - // ImGui hat den Cursor nach dem InvisibleButton bereits korrekt - // weitergesetzt (für die Layout-Engine). Wir merken uns die Position, - // verschieben den Cursor temporär für die Inhalts-Beschriftung und - // restoren ihn am Ende — sonst kollidieren manuelle Cursor-Sets mit - // SameLine im Caller und die Cards stapeln sich diagonal. - var cursorAfterButton = ImGui.GetCursorScreenPos(); + // Inhalts-Overlay: Icon + Title + Subtext direkt mit DrawList in den + // Card-Bereich zeichnen, statt Cursor-Hopping mit SetCursorScreenPos. + // DrawList-Overlays ändern den Cursor nicht, BeginGroup/EndGroup + // hält den Layout-Anker stabil für SameLine. + var iconPos = cursorBefore + new Vector2(16f, 12f); + var titlePos = cursorBefore + new Vector2(16f, 40f); + var subtextPos = cursorBefore + new Vector2(16f, 62f); - var textPos = cursorBefore + new Vector2(16f, 12f); - ImGui.SetCursorScreenPos(textPos); - // Plugin ist hier Instanz, nicht Static-Type — daher über _window - // referenzieren (Codebase-Konvention, siehe ImGuiUtil.cs:22 für die - // alternative Static-Init-Pattern, das wir hier nicht nutzen). + var titleColor = ColourUtil.RgbaToAbgr(0xE6F4F1FFu); + var subtextColor = ColourUtil.RgbaToAbgr(0x8FA3B5FFu); + + // Icon via FontAwesome — temporär den Font pushen, mit DrawList zeichnen using (_window.Plugin.FontManager.FontAwesome.Push()) { - ImGui.Text(icon.ToIconString()); + draw.AddText(iconPos, titleColor, icon.ToIconString()); } - ImGui.SetCursorScreenPos(textPos + new Vector2(0f, 28f)); - ImGui.TextUnformatted(title); + draw.AddText(titlePos, titleColor, title); + draw.AddText(subtextPos, subtextColor, subtext); - ImGui.SetCursorScreenPos(textPos + new Vector2(0f, 50f)); - using (ImRaii.PushColor(ImGuiCol.Text, 0xFF8FA3B5u)) - { - ImGui.TextUnformatted(subtext); - } - - // Restore cursor to post-button position, so SameLine + Wrap im Caller - // wieder mit dem ImGui-Layout-Flow arbeiten. - ImGui.SetCursorScreenPos(cursorAfterButton); + ImGui.EndGroup(); if (clicked) { -- 2.52.0 From fcbbd174b6b9a20a200644114e2523826218baed Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 14:31:35 +0200 Subject: [PATCH 049/169] fix(themes): wrap theme cards in begin/end group so the grid wraps Theme-card grid was stacking diagonally for the same reason the settings overview did: SetCursorScreenPos plus SameLine in the caller loop don't compose. Wrap each card in BeginGroup/EndGroup, draw name and author via DrawList instead of cursor hops, and let ImGui handle row wrapping naturally. --- HellionChat/Ui/SettingsTabs/Themes.cs | 42 ++++++++++++++------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/HellionChat/Ui/SettingsTabs/Themes.cs b/HellionChat/Ui/SettingsTabs/Themes.cs index f501b6b..564322d 100644 --- a/HellionChat/Ui/SettingsTabs/Themes.cs +++ b/HellionChat/Ui/SettingsTabs/Themes.cs @@ -82,21 +82,27 @@ internal sealed class Themes : ISettingsTab var avail = ImGui.GetContentRegionAvail(); var columns = avail.X >= 700f ? 3 : 2; var cardWidth = (avail.X - (columns - 1) * 8f) / columns; - var cardHeight = 140f; // war 110f — Mockup braucht mehr Platz - var i = 0; - foreach (var theme in themes) + var cardHeight = 140f; // Mockup + Name + Author brauchen den Platz + + var list = themes.ToList(); + for (var i = 0; i < list.Count; i++) { - DrawThemeCard(theme, activeSlug, cardWidth, cardHeight); - i++; - if (i % columns != 0) + DrawThemeCard(list[i], activeSlug, cardWidth, cardHeight); + + // SameLine zwischen den Cards einer Reihe; am Spalten-Ende kein + // SameLine, dann beginnt automatisch eine neue Zeile. + if ((i + 1) % columns != 0 && i != list.Count - 1) ImGui.SameLine(); - else - ImGui.NewLine(); } } private void DrawThemeCard(Theme theme, string activeSlug, float w, float h) { + // BeginGroup macht den Card-Bereich zu einem einzelnen ImGui-Layout-Item. + // Damit funktioniert SameLine() im Caller-Loop — sonst tracked ImGui die + // einzelnen InvisibleButton-Items separat und das Wrapping bricht. + ImGui.BeginGroup(); + var isActive = string.Equals(theme.Slug, activeSlug, StringComparison.OrdinalIgnoreCase); var cursorBefore = ImGui.GetCursorScreenPos(); var clicked = ImGui.InvisibleButton($"##theme-card-{theme.Slug}", new Vector2(w, h)); @@ -117,25 +123,21 @@ internal sealed class Themes : ISettingsTab draw.AddRect(cursorBefore, cursorBefore + new Vector2(w, h), border, 4f, ImDrawFlags.None, 1f); } - // Mini-Mockup statt Akzent-Swatch — visualisiert das Theme im Mini-Chat-Window + // Mini-Mockup oben — DrawList-Operation, kein Cursor-Hopping var mockupOrigin = cursorBefore + new Vector2(12f, 12f); var mockupSize = new Vector2(w - 24f, 60f); ThemeMockup.Draw(mockupOrigin, mockupSize, theme); - // Name unter dem Mockup - ImGui.SetCursorScreenPos(cursorBefore + new Vector2(12f, 78f)); + // Name + Author direkt via DrawList (statt SetCursorScreenPos + + // TextUnformatted), damit der ImGui-Layout-Cursor stabil bleibt + // und die BeginGroup/EndGroup-Klammer den Card-Bereich als ein + // Layout-Item führt. var textColor = ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary); - using (ImRaii.PushColor(ImGuiCol.Text, textColor)) - ImGui.TextUnformatted(theme.Name); - - // Author dahinter dezent - ImGui.SetCursorScreenPos(cursorBefore + new Vector2(12f, 96f)); var mutedColor = ColourUtil.RgbaToAbgr(theme.Colors.TextMuted); - using (ImRaii.PushColor(ImGuiCol.Text, mutedColor)) - ImGui.TextUnformatted(theme.Author); + draw.AddText(cursorBefore + new Vector2(12f, 80f), textColor, theme.Name); + draw.AddText(cursorBefore + new Vector2(12f, 100f), mutedColor, theme.Author); - // Cursor unter die Card setzen - ImGui.SetCursorScreenPos(cursorBefore + new Vector2(0f, h + 8f)); + ImGui.EndGroup(); if (clicked) { -- 2.52.0 From 53952717c0838ec470f425ad226a60615a588519 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 14:44:59 +0200 Subject: [PATCH 050/169] feat(themes): optional chat channel colors in theme schema --- HellionChat/Themes/Theme.cs | 3 ++- HellionChat/Themes/ThemeChatColors.cs | 11 +++++++++++ HellionChat/Themes/ThemeJsonLoader.cs | 27 ++++++++++++++++++++++++++- HellionChat/Themes/ThemeJsonWriter.cs | 8 ++++++++ 4 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 HellionChat/Themes/ThemeChatColors.cs diff --git a/HellionChat/Themes/Theme.cs b/HellionChat/Themes/Theme.cs index 591bf49..18eee74 100644 --- a/HellionChat/Themes/Theme.cs +++ b/HellionChat/Themes/Theme.cs @@ -8,5 +8,6 @@ public sealed record Theme( ThemeColors Colors, ThemeLayout Layout, ThemeTypography Typography, - bool IsBuiltIn + bool IsBuiltIn, + ThemeChatColors? ChatColors = null ); diff --git a/HellionChat/Themes/ThemeChatColors.cs b/HellionChat/Themes/ThemeChatColors.cs new file mode 100644 index 0000000..3c2ba1b --- /dev/null +++ b/HellionChat/Themes/ThemeChatColors.cs @@ -0,0 +1,11 @@ +using HellionChat.Code; + +namespace HellionChat.Themes; + +// Optional pro Theme. Wenn ein Theme ChatColors mitliefert, kann der +// User sie per Klick im Themes-Tab auf Configuration.ChatColours anwenden. +// Ein Theme ohne ChatColors (z.B. chat2-classic) lässt die User-Channel- +// Farben unverändert. +public sealed record ThemeChatColors( + IReadOnlyDictionary Channels +); diff --git a/HellionChat/Themes/ThemeJsonLoader.cs b/HellionChat/Themes/ThemeJsonLoader.cs index d73200f..e340165 100644 --- a/HellionChat/Themes/ThemeJsonLoader.cs +++ b/HellionChat/Themes/ThemeJsonLoader.cs @@ -32,10 +32,35 @@ internal static class ThemeJsonLoader var colors = ReadColors(root.GetProperty("colors")); var layout = ReadLayout(root.GetProperty("layout")); - return new Theme(slug, name, author, description, colors, layout, new ThemeTypography(), IsBuiltIn: false); + ThemeChatColors? chatColors = null; + if (root.TryGetProperty("chatChannels", out var ch) && ch.ValueKind == JsonValueKind.Object) + chatColors = ReadChatColors(ch); + + return new Theme(slug, name, author, description, colors, layout, new ThemeTypography(), IsBuiltIn: false, ChatColors: chatColors); } } + private static ThemeChatColors ReadChatColors(JsonElement el) + { + var dict = new Dictionary(); + foreach (var prop in el.EnumerateObject()) + { + // Property-Name ist der ChatType-Name als String (z.B. "Say", "Tell"), + // Value ist Hex wie bei den Theme-Colors. Unbekannte Channel-Names + // werden still übersprungen — Forward-Compat falls SE neue Channels + // einführt. + if (!Enum.TryParse(prop.Name, ignoreCase: true, out var channel)) + continue; + if (prop.Value.ValueKind != JsonValueKind.String) + continue; + var hex = prop.Value.GetString(); + if (string.IsNullOrWhiteSpace(hex)) + continue; + dict[channel] = HellionChat.Util.ColourUtil.HexToRgba(hex); + } + return new ThemeChatColors(dict); + } + public static Theme LoadFromFile(string path) { var json = File.ReadAllText(path); diff --git a/HellionChat/Themes/ThemeJsonWriter.cs b/HellionChat/Themes/ThemeJsonWriter.cs index 97523f1..9cf197a 100644 --- a/HellionChat/Themes/ThemeJsonWriter.cs +++ b/HellionChat/Themes/ThemeJsonWriter.cs @@ -52,6 +52,14 @@ internal static class ThemeJsonWriter writer.WriteNumber("frameBorderSize", theme.Layout.FrameBorderSize); writer.WriteEndObject(); + if (theme.ChatColors is { Channels.Count: > 0 } cc) + { + writer.WriteStartObject("chatChannels"); + foreach (var kvp in cc.Channels) + writer.WriteString(kvp.Key.ToString(), $"#{kvp.Value:X8}"); + writer.WriteEndObject(); + } + writer.WriteEndObject(); } -- 2.52.0 From 15a89dd6e74766b7cbe61e165f22fe94cea53c52 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 14:48:34 +0200 Subject: [PATCH 051/169] feat(themes): chat channel color sets for four built-in themes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hellion Arctic, Event Horizon, Moonlit Bloom and Mint Grove each ship a distinct chat-channel palette tinted toward their brand family while preserving the FFXIV channel identity (Say light, Yell yellow, Shout orange, Tell pink-magenta, Party blue, FC cyan, NN green). Chat 2 Klassik intentionally ships without — users picking that theme keep their existing channel colours. --- HellionChat/Themes/Builtin/EventHorizon.cs | 29 ++++++++++++++++++++- HellionChat/Themes/Builtin/HellionArctic.cs | 28 +++++++++++++++++++- HellionChat/Themes/Builtin/MintGrove.cs | 29 ++++++++++++++++++++- HellionChat/Themes/Builtin/MoonlitBloom.cs | 28 +++++++++++++++++++- 4 files changed, 110 insertions(+), 4 deletions(-) diff --git a/HellionChat/Themes/Builtin/EventHorizon.cs b/HellionChat/Themes/Builtin/EventHorizon.cs index ac3b625..c436ec6 100644 --- a/HellionChat/Themes/Builtin/EventHorizon.cs +++ b/HellionChat/Themes/Builtin/EventHorizon.cs @@ -45,6 +45,33 @@ internal static class EventHorizon ScrollbarRounding: 4f, WindowBorderSize: 1f, FrameBorderSize: 1f ), Typography: new ThemeTypography(), - IsBuiltIn: true + IsBuiltIn: true, + ChatColors: new ThemeChatColors(new Dictionary + { + // Event Horizon — Cosmic-Purple-Drift: helle Pastelle bekommen + // Lavender-Tinte, Akzent-Channels (Tell) ziehen Richtung Magenta- + // Lila. Channel-Identität bleibt klar erkennbar. + [HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#E6E0F5"), + [HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F2C25C"), + [HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FF9050"), + [HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#E090FF"), + [HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#E090FF"), + [HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#90A0FF"), + [HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#FFAA80"), + [HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#9090E8"), + [HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#A0E090"), + [HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#90A0FF"), + [HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#A0E090"), + [HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#F0B070"), + [HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F2C25C"), + [HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#80E0B0"), + [HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#90A0FF"), + [HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#B585FF"), + [HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#E090FF"), + [HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#D0A0F0"), + [HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E0B870"), + [HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E0B870"), + [HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#9890B5"), + }) ); } diff --git a/HellionChat/Themes/Builtin/HellionArctic.cs b/HellionChat/Themes/Builtin/HellionArctic.cs index 38ed5b6..481084f 100644 --- a/HellionChat/Themes/Builtin/HellionArctic.cs +++ b/HellionChat/Themes/Builtin/HellionArctic.cs @@ -45,6 +45,32 @@ internal static class HellionArctic ScrollbarRounding: 2f, WindowBorderSize: 1f, FrameBorderSize: 1f ), Typography: new ThemeTypography(), - IsBuiltIn: true + IsBuiltIn: true, + ChatColors: new ThemeChatColors(new Dictionary + { + // Hellion Arctic — FFXIV-Standard mit dezenter Cyan-Tinte in den + // blauen Channels (Party/FC). Channel-Identität bleibt klar. + [HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#FFFFFF"), + [HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#FFE066"), + [HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FFA040"), + [HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#FF99CC"), + [HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#FF99CC"), + [HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#80C0E8"), + [HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#FFB870"), + [HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#4DD9E8"), + [HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#A8E060"), + [HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#80C0E8"), + [HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#A8E060"), + [HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#FFC080"), + [HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#FFE066"), + [HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#80E8A8"), + [HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#80C0E8"), + [HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#A8A0F0"), + [HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#FF99CC"), + [HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#E8B0F0"), + [HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E8C880"), + [HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E8C880"), + [HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#C0C0C0"), + }) ); } diff --git a/HellionChat/Themes/Builtin/MintGrove.cs b/HellionChat/Themes/Builtin/MintGrove.cs index 727d30c..eab1f5c 100644 --- a/HellionChat/Themes/Builtin/MintGrove.cs +++ b/HellionChat/Themes/Builtin/MintGrove.cs @@ -45,6 +45,33 @@ internal static class MintGrove ScrollbarRounding: 3f, WindowBorderSize: 1f, FrameBorderSize: 1f ), Typography: new ThemeTypography(), - IsBuiltIn: true + IsBuiltIn: true, + ChatColors: new ThemeChatColors(new Dictionary + { + // Mint Grove — Naturthemen-Tönung: Honey-Amber in Yell-Familie, + // Mint-Drift in NoviceNetwork und Linkshell. Tell-Pink-Identität + // bleibt erhalten für Erkennbarkeit. + [HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#E8F5EA"), + [HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F9D580"), + [HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#F0A050"), + [HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#F098C8"), + [HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#F098C8"), + [HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#80B8D0"), + [HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#F0B070"), + [HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#80C8B0"), + [HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#8FE0B8"), + [HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#80B8D0"), + [HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#8FE0B8"), + [HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#F0BC80"), + [HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F9D580"), + [HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#80E0A0"), + [HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#80B8D0"), + [HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#A89DC0"), + [HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#F098C8"), + [HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#D0A8C8"), + [HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E8C088"), + [HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E8C088"), + [HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#9BB5A5"), + }) ); } diff --git a/HellionChat/Themes/Builtin/MoonlitBloom.cs b/HellionChat/Themes/Builtin/MoonlitBloom.cs index 07b700d..3da16b3 100644 --- a/HellionChat/Themes/Builtin/MoonlitBloom.cs +++ b/HellionChat/Themes/Builtin/MoonlitBloom.cs @@ -45,6 +45,32 @@ internal static class MoonlitBloom ScrollbarRounding: 4f, WindowBorderSize: 1f, FrameBorderSize: 1f ), Typography: new ThemeTypography(), - IsBuiltIn: true + IsBuiltIn: true, + ChatColors: new ThemeChatColors(new Dictionary + { + // Moonlit Bloom — Bloom-Magenta-Tönung. Sage-Drift in NoviceNetwork + // und Linkshell4. Tell-Pink-Identität bleibt sichtbar. + [HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#ECE6F5"), + [HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F0D080"), + [HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#F09A60"), + [HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#EF8AF4"), + [HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#EF8AF4"), + [HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#A0B0F0"), + [HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#F0B090"), + [HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#A8C8E8"), + [HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#9CCB7C"), + [HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#A0B0F0"), + [HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#9CCB7C"), + [HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#F0BC92"), + [HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F0D080"), + [HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#B6E297"), + [HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#A0B0F0"), + [HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#C098D8"), + [HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#EF8AF4"), + [HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#E8B0E8"), + [HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E8B590"), + [HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E8B590"), + [HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#9A8BB0"), + }) ); } -- 2.52.0 From f2086865cec69ed6cd47e0e1e2a795aad41a5435 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 14:51:16 +0200 Subject: [PATCH 052/169] feat(themes): opt-in chat color apply banner in themes tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a theme defines its own chat channel colours and the current Configuration.ChatColours don't match, a dezent banner offers Apply / Keep — opt-in, never auto-overwriting user picks. Switching themes re-arms the banner so each theme can be evaluated separately. --- .../Resources/HellionStrings.Designer.cs | 3 + HellionChat/Resources/HellionStrings.de.resx | 9 +++ HellionChat/Resources/HellionStrings.resx | 9 +++ HellionChat/Ui/SettingsTabs/Themes.cs | 75 +++++++++++++++++++ 4 files changed, 96 insertions(+) diff --git a/HellionChat/Resources/HellionStrings.Designer.cs b/HellionChat/Resources/HellionStrings.Designer.cs index deb2ea1..1402a1d 100644 --- a/HellionChat/Resources/HellionStrings.Designer.cs +++ b/HellionChat/Resources/HellionStrings.Designer.cs @@ -230,6 +230,9 @@ internal class HellionStrings internal static string Settings_Themes_Custom => Get(nameof(Settings_Themes_Custom)); internal static string Settings_Themes_OpenFolder => Get(nameof(Settings_Themes_OpenFolder)); internal static string Settings_Themes_ExportActive => Get(nameof(Settings_Themes_ExportActive)); + internal static string Settings_Themes_ApplyChatColors_Hint => Get(nameof(Settings_Themes_ApplyChatColors_Hint)); + internal static string Settings_Themes_ApplyChatColors_Apply => Get(nameof(Settings_Themes_ApplyChatColors_Apply)); + internal static string Settings_Themes_ApplyChatColors_Keep => Get(nameof(Settings_Themes_ApplyChatColors_Keep)); // Hellion Chat — General-Tab section headings internal static string Settings_General_Input_Heading => Get(nameof(Settings_General_Input_Heading)); diff --git a/HellionChat/Resources/HellionStrings.de.resx b/HellionChat/Resources/HellionStrings.de.resx index 207bb35..9b9c9bf 100644 --- a/HellionChat/Resources/HellionStrings.de.resx +++ b/HellionChat/Resources/HellionStrings.de.resx @@ -696,4 +696,13 @@ Aktives exportieren... + + Dieses Theme schlägt eigene Channel-Farben vor. + + + Übernehmen + + + Behalten + diff --git a/HellionChat/Resources/HellionStrings.resx b/HellionChat/Resources/HellionStrings.resx index b34833f..edba1d7 100644 --- a/HellionChat/Resources/HellionStrings.resx +++ b/HellionChat/Resources/HellionStrings.resx @@ -696,4 +696,13 @@ Export active... + + This theme suggests its own chat channel colours. + + + Apply + + + Keep current + diff --git a/HellionChat/Ui/SettingsTabs/Themes.cs b/HellionChat/Ui/SettingsTabs/Themes.cs index 564322d..be66c5d 100644 --- a/HellionChat/Ui/SettingsTabs/Themes.cs +++ b/HellionChat/Ui/SettingsTabs/Themes.cs @@ -12,6 +12,12 @@ internal sealed class Themes : ISettingsTab private readonly Plugin Plugin; private readonly Configuration Mutable; + // Tracks ob der User die Apply-Frage für das aktive Theme bereits + // beantwortet hat. Banner wird nur angezeigt wenn das Theme ein + // ChatColors-Set hat UND noch keine Antwort vorliegt UND die aktuellen + // Mutable.ChatColours davon abweichen. + private string? _applyDismissedFor; + public string Name => HellionStrings.ResourceManager.GetString("Settings_Tab_Themes") ?? "Themes" + "###tabs-themes"; internal Themes(Plugin plugin, Configuration mutable) @@ -30,6 +36,8 @@ internal sealed class Themes : ISettingsTab using (ImRaii.PushColor(ImGuiCol.Text, 0xFF8FA3B5u)) ImGui.TextUnformatted(active.Author); + DrawChatColorsApplyBanner(active); + ImGui.Spacing(); ImGui.Separator(); ImGui.Spacing(); @@ -143,6 +151,73 @@ internal sealed class Themes : ISettingsTab { Mutable.Theme = theme.Slug; Plugin.ThemeRegistry.Switch(theme.Slug); + _applyDismissedFor = null; // Banner für neues Theme wieder zeigen } } + + private void DrawChatColorsApplyBanner(Theme active) + { + // Klassik hat per Design keine ChatColors — kein Banner. + if (active.ChatColors is not { Channels.Count: > 0 } themeChatColors) + return; + + // User hat die Frage bereits für genau dieses Theme beantwortet. + if (_applyDismissedFor == active.Slug) + return; + + // Wenn die aktuellen Channel-Colors bereits exakt mit dem Theme-Vorschlag + // übereinstimmen, gibt's nichts zu tun. + var alreadyMatching = themeChatColors.Channels.All(kvp => + Mutable.ChatColours.TryGetValue(kvp.Key, out var current) && current == kvp.Value); + if (alreadyMatching) + return; + + ImGui.Spacing(); + + // Dezent-Akzent-Banner mit Border in Theme-Primary + var border = ColourUtil.RgbaToAbgr(active.Colors.Primary); + var bgFill = ColourUtil.RgbaToAbgr((active.Colors.Surface & 0xFFFFFF00u) | 0xCCu); + var origin = ImGui.GetCursorScreenPos(); + var width = ImGui.GetContentRegionAvail().X; + var height = 64f; + var draw = ImGui.GetWindowDrawList(); + draw.AddRectFilled(origin, origin + new Vector2(width, height), bgFill, 4f); + draw.AddRect(origin, origin + new Vector2(width, height), border, 4f, ImDrawFlags.None, 1f); + + var hint = HellionStrings.ResourceManager.GetString("Settings_Themes_ApplyChatColors_Hint") + ?? "This theme suggests its own chat channel colours."; + var applyLabel = HellionStrings.ResourceManager.GetString("Settings_Themes_ApplyChatColors_Apply") + ?? "Apply"; + var keepLabel = HellionStrings.ResourceManager.GetString("Settings_Themes_ApplyChatColors_Keep") + ?? "Keep current"; + + var textColor = ColourUtil.RgbaToAbgr(active.Colors.TextPrimary); + draw.AddText(origin + new Vector2(12f, 10f), textColor, hint); + + // Buttons als InvisibleButton + DrawList-Overlay, damit sie konsistent + // zum Banner-Look bleiben. + using (ImRaii.PushColor(ImGuiCol.Button, active.Colors.Primary)) + using (ImRaii.PushColor(ImGuiCol.ButtonHovered, active.Colors.PrimaryLight)) + using (ImRaii.PushColor(ImGuiCol.ButtonActive, active.Colors.PrimaryDark)) + { + ImGui.SetCursorScreenPos(origin + new Vector2(12f, 32f)); + if (ImGui.Button(applyLabel)) + { + foreach (var kvp in themeChatColors.Channels) + Mutable.ChatColours[kvp.Key] = kvp.Value; + _applyDismissedFor = active.Slug; + } + } + + ImGui.SameLine(); + if (ImGui.Button(keepLabel)) + { + _applyDismissedFor = active.Slug; + } + + // Cursor unter den Banner setzen + ImGui.SetCursorScreenPos(origin + new Vector2(0f, height + 8f)); + + ImGui.Spacing(); + } } -- 2.52.0 From feeb1df4eb0890856291bc024b4961716af0f503 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 14:54:58 +0200 Subject: [PATCH 053/169] docs(themes): theme authoring guide with hellion forge branding --- README.md | 8 ++ docs/THEME-AUTHORING.md | 185 ++++++++++++++++++++++++++++++++++ docs/images/hellion-forge.png | Bin 0 -> 37850 bytes 3 files changed, 193 insertions(+) create mode 100644 docs/THEME-AUTHORING.md create mode 100644 docs/images/hellion-forge.png diff --git a/README.md b/README.md index f6a898b..f257792 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,13 @@ Hellion Chat baut auf [Chat 2](https://github.com/Infiziert90/ChatTwo) von **Inf - **Mitgelieferte Hellion-Schrift** (Exo 2, OFL-1.1) als optionaler Default statt System-Font. - **Hellion-Logo** im Plugin-Bundle und in der Dalamud-Plugin-Liste. +#### Custom Themes (v1.1.0) + +HellionChat 1.1.0 bringt eine Theme-Engine mit fünf eingebauten Themes +(Hellion Arctic, Chat 2 Klassik, Event Horizon, Moonlit Bloom, Mint Grove) +und ein JSON-basiertes Authoring-Format für eigene Themes. Schema und +Schritt-für-Schritt-Anleitung in [`docs/THEME-AUTHORING.md`](docs/THEME-AUTHORING.md). + ### Pop-Out Convenience (v0.6.0) - **Eingabe-Bar in Pop-Out-Fenstern** als globaler Opt-In in Settings → Fenster → Fenster-Rahmen. Wenn aktiv hat jedes Pop-Out-Window unten einen kompakten Input mit kanal-farbigem Icon-Button und Text-Eingabe — kein Wechsel mehr ins Hauptfenster für eine schnelle Antwort. @@ -302,6 +309,7 @@ Dokumentation lebt unter [`docs/`](docs/). | [`docs/CONTRIBUTORS.md`](docs/CONTRIBUTORS.md) | Tester, Übersetzer und Code-Beiträger der Hellion-Seite. | | [`docs/LEARNING-JOURNEY.md`](docs/LEARNING-JOURNEY.md) | Entwicklungsgeschichte, vom Web-Stack zu C# / Dalamud, was ich aus dem Fork gelernt habe. | | [`docs/IPC.md`](docs/IPC.md) | IPC-Kanal-Reference, Tuple-Payload-Felder, Migrations-Diff für Drittplugins. | +| [`docs/THEME-AUTHORING.md`](docs/THEME-AUTHORING.md) | Theme-Engine-Authoring-Guide (EN): JSON-Schema, Color-/Layout-Slots, Channel-Identity-Regeln, Validierung. | | [`docs/UPSTREAM_SYNC.md`](docs/UPSTREAM_SYNC.md) | Cherry-Pick-Policy gegenüber Chat 2. | | [`docs/THIRD_PARTY_NOTICES.md`](docs/THIRD_PARTY_NOTICES.md) | NuGet-Dependencies mit Lizenzen, Bundled Assets, Network-Status pro Komponente. | | [`docs/AI_DISCLOSURE.md`](docs/AI_DISCLOSURE.md) | Offenlegung der KI-Unterstützung im Entwicklungsprozess. | diff --git a/docs/THEME-AUTHORING.md b/docs/THEME-AUTHORING.md new file mode 100644 index 0000000..0f7376a --- /dev/null +++ b/docs/THEME-AUTHORING.md @@ -0,0 +1,185 @@ +

+ Hellion Forge +

+ +# Theme Authoring Guide + +> Built by **Hellion Forge** — the plugin workshop arm of [Hellion Online Media](https://hellion-media.de). HellionChat ships with five built-in themes; this guide walks you through writing your own. + +## TL;DR + +1. Open Settings → Themes → **Open themes folder** +2. Copy `example-theme.json` to `.json` in the same folder +3. Edit the file with any text editor +4. Reload the plugin (toggle off/on in `/xlplugins`) +5. Your theme appears in the Custom-Themes section in Settings → Themes + +That's the whole loop. The rest of this document is reference. + +## File location + +``` +%APPDATA%\XIVLauncher\pluginConfigs\HellionChat\themes\ +``` + +(or the equivalent path on Linux/macOS — Settings → Themes → "Open themes folder" opens it directly). + +Each `*.json` file in this folder is loaded as one theme. The `example-theme.json` that HellionChat seeds on first launch is your starting template. + +## File format + +Theme JSON has four blocks: + +```json +{ + "schemaVersion": 1, + "slug": "your-slug", + "name": "Your Theme Name", + "author": "You", + "description": "One-line description shown under the theme name.", + "colors": { ... 21 color slots ... }, + "layout": { ... 9 layout values ... }, + "chatChannels": { ... optional, channel-name → hex ... } +} +``` + +### Top-level fields + +| Field | Type | Required | Notes | +|---|---|---|---| +| `schemaVersion` | int | yes | Always `1` for HellionChat 1.1.0. The plugin warns and skips themes with a different number. | +| `slug` | string | yes | Lowercase, hyphenated. Must be unique across all themes (built-in slugs are reserved). | +| `name` | string | yes | Display name in the picker. | +| `author` | string | yes | Shown small under the theme name. | +| `description` | string | yes | One short sentence. | +| `colors` | object | yes | All 21 slots required (see below). | +| `layout` | object | yes | All 9 slots required (see below). | +| `chatChannels` | object | no | Optional channel-name → hex map (see below). | + +### Color slots + +All values are 6-digit `#RRGGBB` or 8-digit `#RRGGBBAA` hex strings. Six-digit values get an implicit `FF` alpha. + +| Slot | Role | +|---|---| +| `primary` | Brand color — used on buttons, sliders, check marks, highlighted separators. | +| `primaryDark` | Pressed-button stage. | +| `primaryLight` | Hovered-button / link-text stage. | +| `primaryGlow` | Glow / subtle accent (typically primary with ~60% alpha). | +| `accent` | Counter-accent — scrollbar grab on hover/active, resize grip, optional CTA. | +| `accentDark` / `accentLight` | Dark/light siblings of accent. | +| `identity` | Title-bar active color and active-tab color. Often equals `primaryDark`. | +| `windowBg` | Outermost window background. | +| `childBg` | Inner panel / popup background. | +| `frameBg` | Input fields, sliders, combos. | +| `surface` | Card surfaces, headers, selectables. | +| `surfaceHover` | Hovered card / header step. | +| `border` | Panel borders. Typically primary with ~40% alpha for a brand-tinted edge. | +| `textPrimary` | Body text. Soft off-white reads better than pure `#FFFFFF` on dark backgrounds. | +| `textMuted` | Captions, secondary lines. | +| `textDim` | Disabled / hint text, separators. | +| `statusSuccess` | Green-ish for success notifications. | +| `statusDanger` | Red for errors. | +| `statusWarning` | Amber for warnings. | +| `statusInfo` | Cyan-ish info. Often equals primary. | + +### Layout slots + +All values are floats in pixels. `BorderSize` is 0 or 1 (no thicker borders look right with ImGui's edge anti-aliasing). + +| Slot | Typical range | Notes | +|---|---|---| +| `windowRounding` | 0–8 | 0 = sharp upstream look; 4–6 = softer "app" feel. | +| `childRounding` | 0–6 | Usually 1 less than `windowRounding`. | +| `popupRounding` | 0–6 | Same as `childRounding`. | +| `frameRounding` | 0–4 | For inputs, sliders. | +| `grabRounding` | 0–4 | Slider grab dot. | +| `tabRounding` | 0–4 | Tab corners. | +| `scrollbarRounding` | 0–4 | Scrollbar grab. | +| `windowBorderSize` | 0 or 1 | 1 reads better in dark themes. | +| `frameBorderSize` | 0 or 1 | Usually matches windowBorderSize. | + +### Optional `chatChannels` + +If present, your theme proposes its own chat-channel colors. Property names are `ChatType` enum values (case-insensitive). Unknown names are skipped silently — safe for forward-compat. + +```json +"chatChannels": { + "Say": "#FFFFFF", + "Yell": "#FFE066", + "Shout": "#FFA040", + "TellIncoming": "#FF99CC", + "TellOutgoing": "#FF99CC", + "Party": "#80C0E8", + "FreeCompany": "#4DD9E8", + "NoviceNetwork": "#A8E060", + "Linkshell1": "#A8E060" +} +``` + +The user is asked **once per theme switch** whether to apply these colors — never auto-overwriting existing picks. The banner shows up only if your suggested colors differ from the user's current `Configuration.ChatColours`. + +#### Channel-identity rule + +**Don't break FFXIV channel identity.** Players have used these conventions for over a decade: + +| Channel | Convention | Why | +|---|---|---| +| Say | white / off-white | Default-readable speech. | +| Yell | yellow | Urgent broadcast. | +| Shout | orange | Local urgent. | +| Tell | pink-magenta | Whisper, must stand out. | +| Party | light blue | Group ops. | +| FreeCompany | cyan-teal | Guild ops. | +| NoviceNetwork | lime-green | Mentor channel. | + +A theme can tint these toward its brand family (e.g., a purple theme can shift Tell from `#FF99CC` to `#E090FF`), but **don't** flip them (Tell suddenly green, Yell suddenly cyan). RP groups and combat-spec setups depend on the visual hierarchy. + +The four colored built-in themes (Hellion Arctic, Event Horizon, Moonlit Bloom, Mint Grove) all follow this rule — read their JSON for reference. Chat 2 Klassik intentionally ships without `chatChannels` so the user keeps their existing picks. + +## Theme families + +Naming convention `-` is recommended for theme families. The first member of a family is the lightest/brightest: + +- `mint-grove` (current built-in, light mint) +- `forest-grove` (planned, dark emerald) +- `moss-grove` (planned, mid muted) + +Code-wise families have no special handling — only the slug naming hints at the relationship. The picker may group families later, but that's not required. + +## Validation and errors + +When HellionChat loads your theme: + +- **Schema mismatch** (`schemaVersion != 1`): theme is skipped, warning written to `/xllog`. +- **Missing required field** (e.g., no `slug`): theme is skipped, warning written. +- **Invalid hex** (e.g., `#GGHHII`): theme is skipped, warning written. +- **Unknown channel name** in `chatChannels`: that one channel is skipped silently, the rest of the theme loads normally. + +Check `/xllog` after a plugin reload to see what loaded and what didn't. + +## Testing your theme + +1. Edit the JSON, save the file. +2. Reload the plugin: `/xlplugins` → toggle HellionChat off, then on. +3. Settings → Themes → click your theme card. +4. Watch every plugin window (chat, settings, pop-out) and pick something to fix. +5. Tweak. Reload. Repeat. + +Tip: the **Settings → Themes** picker shows a mini-mockup per theme — your colors are visible before you switch. + +## Sharing themes + +Themes are JSON, so sharing is just a file. Drop it into someone's `pluginConfigs/HellionChat/themes/` folder and their plugin picks it up on next reload. + +A community theme repository is on the Hellion Forge roadmap. Until then: share via Discord or any pastebin. + +## Reference + +- `docs/example-theme.json` (seeded automatically on first launch into `pluginConfigs/HellionChat/themes/`) — minimal valid theme. +- The five built-in themes live in source under `HellionChat/Themes/Builtin/`. They are a good reference for Color choices that work. +- [Hellion Online Media branding](https://hellion-media.de) — the Arctic Cyan + Ember Glow palette that drives the default Hellion Arctic theme. + +--- + +

HellionChat is a privacy-focused fork of Chat 2, distributed under the EUPL-1.2.
Theme engine and authoring guide are part of Hellion Forge.

diff --git a/docs/images/hellion-forge.png b/docs/images/hellion-forge.png new file mode 100644 index 0000000000000000000000000000000000000000..f4b169b48cb67e7ee8dc95607068a30043f7db3b GIT binary patch literal 37850 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 LNkvXXu0mjfa({cg literal 0 HcmV?d00001 -- 2.52.0 From 916640fb60fb313c604ed4a7f3637911ec7005f3 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 15:00:08 +0200 Subject: [PATCH 054/169] feat(brand): swap plugin icon to hellion forge hammer The chat plugin now ships under the Hellion Forge plugin-workshop brand. Icon is the 512x512 hammer mark from the Forge logo set (was 256x256 ChatTwo derivative). --- HellionChat/images/icon.png | Bin 54069 -> 36142 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/HellionChat/images/icon.png b/HellionChat/images/icon.png index fce75cddc38bb8dda29f6a8cc592896661f557aa..8c2430cb76a7b4ae9b214ebcc9bb744b000d6274 100644 GIT binary patch literal 36142 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

    XZQT7rfO(uMe}vh3_!KQRbj>Pz=k>&MSR$ zcD$eua(Z?HzqC^y&bZ6!))Ljmsy52p5i`k198fgl@J!gHFnWl&NVGzHURo0NDzj&) zq%}Yqxa3>t!FacK4GjL7uR7kxDF8@tUytK2HJ#!8RM@$?i`h+eq z{@WpDF&#iwsYv6VD$ClZ$%?IwWrcuHz;U0{tcaqz738f0Rcx_%9`a`!-46ne?u*~f zJ1x?>*$+NA67el?@8Y${)}dr28+KhTkMKNP@-qk*P|@ zZ+-sD+oj!QkVA+w5pL1Z12q{E*$rupG9awPD4$$iqQZFh^5$JW1eQDK9!rwrtbVNj zaOibVIeM1~zx|z(vX+#98jndGf^cgP!(R@68_1hVQzRMa&w8wID%7)j7nq=U+(dV6yJ?y2F)DPHnr*D>z zU#tO#Y@7F9m0sLM?tI>$4sw1l%ElTXiM%jf*F}re=CjwfM8BUspk9%kcAwAI^YpZ| z&-3qTpp$CUoQ`rqgPA)Q5m!R^orH%`CwYj1zEQ5*kr&F;Mp1~SoBGVMDA=yBEVr6$ zpW1ULj$6qyA>wp$6n1AO{k=x&>MlWK>fp_j2lpq$Cu=O^#I!2hw?^W&SjMq(=gkJE zZgRS$o%i&L%Bj6rlm2dN&VG4*cv4=EXzq=#b=gE;U5T&`HB?ci>*x)y6X3Z?aXd zFrVt$h_Qjn@>Y?9BOZ=CX)&_-){{xLdT=yXnhjEsyV)GsG~y}&8s&ym_>maLj&o&y z7Shn;-cLjMz!L{WA&}0JbnFov%FR>IQVScCQg%O2NcAZ(jdt9KQbEg zphb=&&FPZj;syJsd^Jk=_6zeK{Dt!mo*T~OSU@MRFTg@rHo@rFTbcS(5@eQHtzA&tv>V?`hrFmiR z@PBb$3dr{L!E9?YlbwVdwaY<=FoLN?m9?8PTufO!4fMmX)j&b2aM9U|m^A3zTOeP0 zp>d#%aPe(FI=RFbMN5&JymWfJTl@rl;m{G3QrZ|=`P=^PrQ|Tqdi!PaY9>dQipOhi zxD{)JD3++8lUY7U?b+ag~hUi%^&Kr``Xsj@4+gxIL z=3Mgo5Y+u%!nqAqQ$5#Hh^VDTSe2$4g_-QAHN z94fo*94yNd&C5ALIR_kWQxq~5PGlk_@Idhi+V^g@(+G_`ci@_Z~{(@he-cON);U!`QYpSx>j}|XJvDATHQ3rObq`cI1cTh%BSr&Q7 zFLl3j3Kq{T`RRMds=dXc--mhVS-q&7IT|bEvVrBe>jNYA7wtEZBUxdU+;skToyo{3 z2zP|@LVJc6=|*NwDeNO=+CDpJ(xg@arM8X&)iNF>FAU7Bgk2Z&FZiGJDQQ~F;E_|!$9{G-5T{cAa!#!x34lr8ATh1(3%D#Q#g3$T0 z+Vo^JRiY;;r8ha3+*h-#FYzV%6O%1gRyfzry%cT&WlSb?qI$XZS8{ca;#G9Sub&i? zJAk!y|8Gr`1nl#&YmGNV6(Z+nik6{%duUNb$!nAM4v=dc?SlL)j=iuaoGzCuTqzf7 zfJjmH1(|m!$!Njd-)xwL|8(j%a#y9kRMpAd2(v>1X{k0mG{?9Dlwit_8?8?I8dSp2 z@lFNQDL$`>s4idr`RioNp>6J&LgpB~i?t{p#j>2V`^N7}>erycm$+mx+)AKefE(E} zws)7q2u??Wt$fc<01EdB!dTrP0k&HG9$Ta1Z@ zA@uOWdz?7cUy#Qy*ZERz`*_d#$8s3COo?7h-K@GIA>=lDR9xs%{2(@}aO^yX$kgVf zQHyG#jS3b;^9}~N6~qnsW`|ew-;l&663Y7^97#{FS;&iHvZ_c5oTk!B7>brC^fzf9 zz8s$1ZKT0XsE@J3$$W)ube`tmKhdda{8gtsXZGbByG_IOo-)lV7B%Gwv;htQz*H-! z{u+fcV8~g2Os{IY64?jO6u!! zMt0m!4FQ%Rx6|O84$(pTX^ivlG_V6FHiC7PZsVnDH~7~xIwee7FUeMBo>RZRIWzI{%KkmM8AK>;h&LUGK)0QO@Cx}TeMv0?Qb4{Vby;m>gSWjEe zQ*mKg|6R2YVhY>^Y;0`e6BDTlufep1w@t5>W=Q@p8Q4}7306Ls=V&Jx}xoBeG) zIPELwb43eO1i9(1u(Vc3-+9eXg!%c3iH7iy!FSsWe`&^u@RtXY9gD>MezpEb)Wp&c zE1U5%4?4?3Q|0#x);q_dSMBdur*1m&h3tif)~;2EAmUFx<2UC9jK#X`-LXVsHp3Ar z9AT=&mtQ~ZpeFI(KJ6OjG)|eTPIEwY<>j1 zQ>1&{iHni?ChCLtA(4!JH^NkmQE92KYCVnw2{S!5Wz60s8TWPVInrcK!X zpnxV>1+(vZkKX^5{H=-pr1_K%y8U9~py?NK;A0z1E_W5x>)aOZ-$E4!d$RaOOqi*$ zRuJy=sw66esRWOF)qGpBOk`U24O-OkC{*mL70fUA{^QxmNpicLeeibLvD?XHU);k@ z;w-#i8vlQmIjIx)S_aV?4H~)3dQOogS^B1$GaL8oMMDxwv-VN2Q zwL}VbO)jucodemKad@$x8y2Iq2l2!x4Ny>QOYz^wzn!X%XI{i2vy|186SH`YFU;A* z(78PX(46U!#S2}qL=|*QEghamuw<*Oj*7AIQTgZvlwH5)=I03Qg4al$=}frAMyi)X z?%(@u-PWd@^g`|W{U)ye#)ZDJ=2jDd+PyMFPJEI6)c1*A}V#?EbBZ$V*+n6zRWM zKRz3Qw=Mum%+n-2kK-GJO^a^OsmZEL+N!MN(i{Q40D5wTAb5Bk=ra^|vlBsTo_2UCpD=DTNaxeC}AUxOl!AhauPTxkChvv4e3? zeyt0zDIYClwUTWGh!v3E{mj-K*?16%644Dm<th?Tl zp?}By6)JFH#u+cLj5TbdRFM9DDrEj9K7n9)j}jkIXE*APq3nhaH(0WyR>x4~crr3KAzEX1p*=Zn7^5cpq;4S4Y=`)|XX3z9T0d zx3^{A7G&wLz+L_n;|&WVRQJ{)4zx-z$9jk0g=70+oQ^^CYGI**g6s7(b&7N#S?mIL^b*3M zcWiXIfZiNmGZ1`C@#j{?0{vx1(ubH z0eO*>weYF?=v(HhgN(zGFXUkM!o`W`>zibwV{c}XKV9E!B(Psz%PfsG z@llULyEos)sSYi4^hd}Q`1!-Sv==O>?jPDsL?mjZ&uWvd9>nIj`AbSWlcA->AQ$6< zZtVMssKvo8Q9~NwOVGL8Eb9BS?r#q#1RZmHu*9CMgU2mhI)X(k9kJe5;Mz{#ZX^j# z2n!l83ZKeccgFp_SVWGzcY=B_B7NhJH-vmLtHGZwJKiUoLBEH=K-J<|2V#%FJXs>Z zTaGaT;{cjP{-0CR^>(B^)qrKFt#3)DIIStOb_I4;gG{`#|*GAVMXL z5{~TeJTZ-V%-BMi30mt7;B~ii8^U+U<6jqz8|o#R)c$<)oKfk|`VgE)?h&HN%9m)| ztpye&?1Z8{KMuZ%CmI#$-QvLe%FT!eI8=6$w*MNFqi(>RIsVHFCi;t+caCi2#c$Lk zsqoM4@O*@{KxoNPNIHrgQQbcL(m}4==}@uH|3znj)C+a;Eae|$I&@bH2y*)z^r~9+ zqgg{!#abW>*M5@4;gfS`==RIw$pSiBY_GSvl_w`%CI_K2w?ndfh;zy`8Y5y-H=i)C zFg$$7o^%?Lo=l#X@EmxYDe-I?;$!2b^|`;k&0d@R zM7DFAb>3O`)8?i`IpdW*$Zm+}WSNmX_zHJ^WzWkfxgd-1lLD9YoD)K){8)nLLqn+co$c@pOC`~_Q3iN zfK>F(^RcjC3lCq|XTL~??=z_Z??!1RC=D?Y}Ctns8-pzhZ}--+rpq3(GVh} zFoIRi=qXhVdRzBi_lBAF>Bn?H2SmL&`#^t{zjgi%W7Z1}%dp`1zcX%bj83XKZyAJ% zakYyitOBe1+|~%&DRw_GP?V+xU%euNa7Mn^1D@b2=fg)%96k~MT;N2lvd@*vI2k;C zFHA;&{-YB8i>_7rKwf+Ze49H3Fz>L&aoKcs<+Ok?b9xS9PcrfHf#{2 zc>^6S`$!`W*jtMmzkxc~qjfcY9?$!CcT05nVYA~XlP~94+@A7sp`#uY5}JTaHvzo@ zhD3<1{maFGs9V`?7131vqT#kK*X22^@InV+Pj+wX4$bMnX;x(Y-#3R-uqVId4_>n& zHxJV-QUHApIYTDC@wu0IQW<`iYi+Qr#BWbFT;-jbq#j*i8;KMo{nL+*-gIzWxs9Tx zA{AvyIfD6zK_L<}&>?r68H)9m#)yoo5+Ae~>(0_`eE5h5gus5c`DV(sX=HGa5PXfk z;kj*(d|>@nl@3bud%EDByq($jbDODUoZCKKR{U4Eqn~PLe?Dz9Xfd71wY2%!xhZxo z*&$vqtFfa!)%{b#-w#YcJTLl*YfL{Xsr)JnnI`MteV!nqrllJ2r&ERwYC-o*v{)>3 zWucS1+b`J~;N(b{dx!s1gl_vrx-%`)n^iUW)n~VY*udOkD|cqf_XrVPpd!Lw&RcEty?aV14j@fgxKx60 z$*D}ED4g*8jLwvP?1~@1R2>ce3DE7hoL=v1H0^iZHQW!yZ^Ao4La%aOph-A;d-X}- zT>s&v3}_8^f~~B<*f8o?f{zCq4Q|qnxLXz)&uNt+&+rfsvHS1uU=NTl$Or?|im+fG$LbxPF+iRVRb38-nii$X%=S(v26Le16- zSjGL`tGZ4?a7wY<2wZh8B1A)9wS{s<8XE$c2R>l=CP^2A6Om6+QD9N;U zE!y?OOy3SMGeVhBAj%Urin53Cn}>_MV?^l422n57Ii6gx{><8LDwelB|P z3m<1sBc}gDSjMrI#Kt>Ib^;88du9?ZeJ39nm@pxF00Y!8M-QAW@bf=-M(DgmGasN{ zEm)j=D$c?n_oSx>5AKmiUf{R8?)|@`ZX5~>Fxs{ZHxbbBm*V7nZgBI`QQgPR`Y8Cd z)v2n=4O1yIgf*@PcYy1P)sxg8WDEhbnIY9-!?M*KT^T|LHEwIGAr3*$B(N5b9oZ0BKHBKXZiIu^Plh2 z(B*m&-n>>;htXCoNY!8~Q;KLK@UVv+%a{!rrfp}slJZ@jbswt?>mRNcla9E7lM$E8 zREFuof3+IFaeP?Mc;0V)D)T2bDG`f{#!R#3Nz6F=LObKD{Db-ieFpxe`TyR#Pra?* zBA`9%TvyzIRn4pqUjdhUHqNdNSM%hJE@Nkj{CJ-EO2;C2{IgSi^vDlg8n9iREJ5;%P|qXm?%9f8rrnBvEKhY?uv z>{@X*CF0BMD5gjLyl0*=+OX6JR?I)V|MK?KcffLLWp@%o7)PcJ!*r(iZ{AP)?Gt8@ zFguv}_3kGv|Lq&EKb*tzNBVt|+C!ihgJmOuoxk;#A9YtQn9DhbQD){|JJC&yU5^E- z7XJ4>xL=s>!%my4?E4fB0u^6o(qzz{(4+le<3o)*>Z$AfuK_b-Ae+}N&eFv&T=K0ms3=nZ5>gg9LokfVIYqwi8Sn znkwI(%s)E$(=J)#c!oW>AHHu=YGRZuW46@+ zj%m*lrZf1nZ1Y*4{`6Z)$~RNsnK@H{i(!uM{9I`)SLB~136AwIpwXY!`;sw?|2^-| zd{gqTZCXP+u*Gsi`bVea-4x$*&+R|W6aBjZm|cAsb}}sEaG&!f?9N=j;|1RzH}EqU zciq}n$_YH{D7;PNX#TFvDIXsdhDGPPGy&6dn9viZ8A{-6Q@Q<~V&iniVyA@*WLC!S z%S}jV;ZDdr;bC!WZu|^GK7(X8cTjl}rsSrc$OBwF#P!C8V-Ir$(}vu2uBU^UmARSk ziU7+>ew!I)e-B9t{c+;hxCxlrMIIdz5SY=D_Cxl7?E$%k5f@rcy!*$k!^rnX#^ThO zeY`rJ`We?jsZ_lSII1L5cq#gFSGDm0dlnhv8)-t?toMX3{XPdAGXV~k0K@L7TO>oU zRJ>Fl&%Ojn-w)ys4os+20HuJ{z)clCfs@uWs66GnA^SrM*ls?{vW9_w_wVUv-qgOU zbN;oA1(^QUSZS~&bWJ+TRq#B@IF0$dPS7o>G?p07J;0T4Gw0B1Vz5&vgzxg^zWarCuG3=9maC9V-ADTyViR>?)FK#IZ0 zz{pJ3z!Zo=3=OSJOs!0ewG9lc3=C$L{^&u`kei>9nO2Fc!NAi3GPy)#hn5V#fp1?0>z=YTPaYyxPSAR z_fL3dGL!spC-?4i_ugmiwbqH%(Nf04rosjQ0C=h@P#6FJME(Ql6;lc`uWNwE{}x0T4F& z>|0z49Wue9-??lOprMdp`RhO#7HkzdH$<)7mvQd-dW$CP^^Fp-n?N3OvQbHTqWp2z2Jj@ zyx}1R;*sB!>uRA#?o!{_dtCFjC3)>pJK5`z(>64Vu`GfFMYvqEcQpRX=4ipg&G~aC zaWkFRCzQ^C@48UfyeKdJ!oc>9wGaZAK6|iY!_OR_6vWgXu$TswVr?Hr0yJr?y=5?;Y zu<2R(`@^fXSMC?QVnc{hAV=9|yR%D~aqzlZ?u*~!@X+~az;i&}jfW6#Z7a`bjW{Iu?D_n8k@%X*kqONgCnufAkQ(kQMre|% z<~Nbf`6flx7BQA78zeP2IEWbQQscNSdWjpdK&J{ug^g(KN)*f((!UP-NrB2F)uqQ} z(rthB*X6Q&rgig|!`YH{h-0VReMFa9jw`@LSNi6aK7s<3u`r=GAjZzw$4A_~Ew}oS zIAwBAhliw*9+b>kCLkm87&r7wi8*Z;0?yl)n$XR1{LF@jYwqcZi$l(LLgh1SQ|(wL>lpYx?&kTH$L9S9iWIiu(HE6aZ8PA|03l*PWhJ-hBoP+wjmU z$!mIdq3;tu%P>CUk1)%6l^iEAj8|Q+kkL_n)yl)JoHb?~_^8cihzpQ7O}$CFVa=@5 z|459FyaN;@hKv5+SYR3KKM{ecWKS+Mlu%4p8VrW8=|32AHS6RF^Ad-%I}4KY+Zo(W z`0|Sc2~OCND8CMH6Y6!L%C;E4vP1wi#G$)iE&unwHvfBaTx!fzhD|*{ zfPNKig&vo{;K^ASOZpA~;f{V{+-*&`PYSv5;P1w!9blpgEf5>b<;ZX!-Nk0XIwYY` z@uQRa?=2v6aLm@uDnbyQ+|>Kn*DMYzfK+S?ux_aYO@RO-cmc-gYGu|TAw6ioQ28IO zChX2@PihT*KLr%2FXD#cq|l^O3tu%X@d!~G$u}MrFho+L z9qQNz?2J50<*r4=G@s8GyoWjY`n#CXe{bVy6k}k*WGqYv2`?aSn~6pU!ofo{!8$AT z6K(ry0QqWNa)1^XTIODc%lSc1v2Fllqr{Z~(f1*bshFgYRdy%=Xh4BEa7_t#ON>e2 zov6wuR1^hlKTJYe{hxc&OG_WlU;3PfA($y(hE0AzusQ(t@su=4g#j$H>ui%C$&xo* zsKWub@CcE9BR|YtmBIDB4HqD*hPnJY1)f5jyfElY?6c2nsX$Tum0AaOodVWxugS@g z@gquynOj|WQU8(#L&r8x!4{-ggB|$mH>62FRm$HmY;vGz@{z^6(tKbJKyQqlL6_F~ z?a$}m9>7q#Z@y`4bG<0Q*S_6;-Yr6Wt^L2%K|308SjPa|-h@1H$Vcpn@cL7NQG zGD~vTPF9l+0fn}icgk*jJ{bDy$lBBmfJfHN|nYC z*#eAA0#pkZaJcCKh`4!rF#^>WhoT?hMq}*0>$TFbd{`cA5!!pZ03P*Vv4 zkxau{KnmNAQM@+W^PTLe<4_~rXF#9i&rnw>hc#af;S$afMbz=W&LIXQxlgUkpqA-{ z!Y_J%3c+#sa}a2b-;hZo=GJlf-ys55B54GZu?7sozr4I;${SwH%jU>lCq||`=o?e< zsSwU*%Qe_j)hXBVQ1xz5gD22b+fCqp8LOoMb zP!5d$%!L$UtrY(6HWnR^f2kQFLBjoZ9xR0GZ#lnA&&_>Y@kCF7$YTOI-)aE<8HwEr z3zw`LkUyB!E;zNo?Zg!t)nyA>s^vr!{j4YkDeRmOgYpivPdSVk5L;XF2rwmj!Q^22 zUn?(T&jF~ixm6<;MjciT-YFhJCN_&-A6E8o|F8v33xIkv#6;Ag$uQ9)TFRhZwH@Tj z^xozx=Mz#dLelaiR2YS5ke|kF#!+A<3w)tL=#v}^U#R&?Xnh(7NgPLp8sH)yK&!-% zt`>RTZA^TP{l9>8{}<5qh%C@k7Vae7A*IVhVxFL|S|~gi<`n}%_^PO!bEBVa_kEd{ z{OX>^(NoTwd53Tmix1>0;0aDKxg2v_)*ha$+@Hxi~-QhnUI1Ot}nGRLJBAdp%547)=Gn--`x# zVe?(bR!veM!UVxnk+BAkJDKzyD5D(v?TZZo;^dJjNuoH7A&!V5!B{NXz6l$}$!Rfn z-;a(W0f#To4#%12e{V|AK_pOsI7WaNQI@~g7Nk45VVCy%a7+vZnca}7LWXv$zv;nm zTy77Cd6{ z3IxT(#7qXSkYRB+ZfiM0ysxgrLu2av$5TWGjX~zYKHB1O1C|~igxIm#VIz>xtdL2^ zXH1lKDXv2rpKqq|_ji^qOv~NimgmggRZp(#Tf48Q)2B{xQ=aHzuLaD}g|cv^q@?7- z#E2oepU+_c_yvEPe<%N51hRQ*jyBpzBthD8H@;+P58E{CA0GgV18D$_g3~=mCj<;2 z*AB3yr)#F`zCu6??Zr`i4g9cGV9;0TkT?=t39H17yMJxcI?|8Q|zlm<3kai(F8K%%9?wiq$J&?K zf3g0Rswk9Ko*oAd+73{w7#NFCB(R@+n(Vo?msUk&%X`XT=~nAic}F1xD%ws;c05M? znACFt%JH(!W;}B5K>pfN8L;y@ z0=TuTnG&fV=ZA>Leh6Sg*s;zT;4*_224@hB&mvGSpZQ7tPk`p?VnqtDngB{&>xzB0 zvI=oogoe;}$=;tJ_i1Tq#)y5?Tbk-7OZ17H3bceOB|T}Ty%yn zW$xfaqWJs!%R^Wm_=|?e6j`?4!wGG=^Rg7JeDk(9+Q`k&I6-|nK7?C~>3i=VgDt(w zU2LE0am48OljeJPWd=X?8GceCPXCE~8WWQ>Fw7HvXDlECcU_hwPrNA$URg%AkQhye z#EA)Kz`o_5s#4fpceiY7P0r;V@gl#tkEc2y4kL5?shN(!?0oV1)D14!`LFdN~lYY==pm^TjKcDa}EI* z7I3x7e^fOi5nZ4DSLd*fqyHQfpazYeoP7c^GdE9bBV|O$g`rABN7=xj4!04Z1BRrd z0;mdCVt?z@>mYg`j;FnkF8I5biNdk1v+kf}rEmDYq{a1^{KQ3QJ&P9g5SBAAs^UOJ zoOxo*TeE~PnN&ELNG-4!StGz_#^h@zCMgh-*!@68sJFEz$X1!9l(^Z#3EAIDzozeQ z93C94k|F%EG@|bB?}@?)tZ*UK79#AqA&{8N(-$n1V6GSeDY52>`f!Gr_UlGrtoi`% zHSuDa4JC~8o#|hX@voIzCDAxy7&0<&T8@>Mgk-Ek_Bb$-cH}v8&^^7qdOf;OOVcs1 z8SD3V_XZzQdwBnM>j>|Ij0HmzIn|GF{x2|%lhad0v79D;1|tKUa4=pE&`E^ia{@E= zd=J5yI_{vt!^yh$(adBw=C5K@*7xnF5XrEo!8MRGQ($Vyn@j2bW+d3VET9Dm2`nRA zf2S8`%agFM7y}(0ec+%XJuU_6ypJMOdkfS}mdg74TCWcK&bu#Nq;sZRmwz5@k8a@5 z;+;nn1(VAwe;#4=^!7&K-Iw{15#mHN5mH7oOMO)aYR5tDWF~vJ5KK@oD&UXc4sDhr z`hgyH_o>luy6oTds(bQK9NE6}p^V@@!>&gTDXfx!`Ut;1|TNnL4#3KVvb7yo+eG&vA%!c8Py&;qIRj~MQztn3&WgResuq4 z`qDuU@@_F5?PHKP7fBFQ7S4Hw)nh6|2;defQtN_32{vxFFl567rN2P`a3Z~pd-|K>Ft8y4>O9?Ly0{htB0BzJ|ti#V@PUhMP*QtPm!0(h)Vzd zHL-2&qLTqLkj5Qh>NM1||J}h7s}4vTrQDGpJu$(@F%%h7(S3y4>%9%;)sUOz5h;#j z0O^*R4~Js0l>7~gQAQJu01G%;{;T6)EsPNJDiJZ%1dS*>AO-wqcvyCWla-uADd5#gdpa*B`pvgZmgF!0F7z+!^lv zY?}EAmi3sE7lp;o-KkumkArQ#B1mZ4jf$}61SQEnZZ_|j`5pw+TMkg|>yRgk;Y6f- zH{fD3iG25{6?UnJd+KC~&q0M^kM{VZ2|&cOw7aADV z#E96n_%)LjF18Z~GJ#AZgJ=*mfKlKNF8}iG(4n5KC;F&)s+vQ1R&rV@!NGn-ae4XB z$$dSQ3r;hgE}@PN)Abz*g&aCKa^-lC4SP!=z|P8h#P+*N`OK7(c=Q7k5kZ{19Qb_F zv6fzNg+N>}SSu>I(g~L0bS!Xhl8y(xK+&9lJjAP&#qDyx)9HE_;#0ETN*rP97sSw~ z+?0z8I9X;$9%6GMCPY*Tpw}(p&)?k33;Kp9(}QF&;qx|9)ZMdQCbc~w3~U!FrLEiV zXo|0?siWNWG|sO|G-Mx)aS5H9F4jF*RlvqWZVD2gRIIr&;^s(VQUc#e%QwbFqtKM2 zcqZ+9t%rajvhnCQh~e{>JbSpZ(l+I3`(B*U2PVw z6TJJBbyOU%LgYRJ`>Cm(I5YG8BN9&D${m$V-+YjtK&#;g3aiy#WLA5xJ;(w86%Tk= zf8t~8;`8%QR7}bx)%-dn`N@`Fj zDA4Rh;v+dh+E@Kv=GmZ{GdM2bVi2!RNM=X@9K{OIPX*8Augm(HVjB*{7o?fU(G6$( z;#<}mK@Fuc56qFr1G{b-{_OGC@L*+8brkiQ_XM2&gK?6StcRq+bQ*rq^Lv6r6HS`j zdI@#v9A$WDxKt8zr{=+Bu~rjiLs3(!OnmJ=%dM)mR`zDs5c}6L{C-7S)M^ z2nFCbZ|FZ+Q6Pu9A1oZXLhmkwMe^!~wxkt$tZBq|QqI`uZkmpr^g)4jdVQ~&sJvCm z&y%Nhd9YDp?2cY5b;tq&OYhiI;qk`i@g#MS+SAyD@Bf3H5O4y1p|RI@!OjqUeRi%4 z>`ck5c~r213z~dx#0rEhvaN@3x>mt=V6Dg0rO=qGk07i+%g+W`-dq`K?!JoX%lj&l zF~p^2uqT(iLtAG`+2XI4;-c|^v2QFIyYiRWu(OqTU(RF**4EZ4=Inz8Gd&Wf$%$gm zzTxlpZ0>ne1HDDX)4f_rzGj~xC z;UhzZ8e>wLnr;(JweeZP;-uayK!Iua_!9}Oldf)I*~aAIcQ`R7cznAsiCpfDuux2- z;__5~x(0nJgWi9nJZ9ZAiv5bpSd1>HTe4T!pyDTwcAZCG%E5g<1zs7exoy#)aP{a_ zQT~Jz%XHQ9^qZ{84h{GF@~bByv+dkA3ee{+Oilj0L0#nFaTnkMK)BKjeH3|A2aKH{fv}_G zrz0^#?`eZ{=Jnmk6jxdd2LA~P5Ao1tvbTpL~u$}r_M*+<``$-d8NDjuELTvA2 zH9ZLn)n50WTHuzPY*sF}iHgOa#{VDx!uHeFW~0%*vp6O3qOyF zpAqKcDqsWX`WA5XU+4wk$OKJCMZ~R|pw;5lWXN$My}aO;$k&IW_(Eei*pW>B0Rc`1 z{5u2l-+qc;1b5^piI{>T3c$A?88IJ@+tgS5_}Y)Ccucy9V6lyQmZ3WwwZ?qmF+6l; zIo8F=hW?M$M{yHEVT zftxz4bL+4;ilbQ>8enC5v@xT;)!8H&jrS^6(WQ&VTjQ=9#*Pv_&h0(9yYc^CWFALH zyC2Fr>IbxPc9PaY1lE%`lx$r(FwG~67Kh-9_W7Ei6c#kg znKq+IG4iVuCk~3YHcA_i=M^5N=6@(cO|Jp}%c4Hb# ze$L(%u7~bgj#7bQfV%lXUKYz5gZF23FCr7dhp&dACVQCzM1 zX4dfc=aW9ew|%WoNcP0u%F1r`Fs(_D0O%`I;qZ7hdDCK&hS77TPf~Msww*RA+ zGw^R?<&7Wb%_FS-HcxN4N-`<85vc~e+>JB8SiO7nyz-Uv|6l)u!57=psd3lT7h6ByeMLKzxOGh<8xI7nNLIiP4bt{Rwff=eEXkI=ie-}jmB5}RH zkC2T-2{z%W@z7$;U>(vsnaAyBNK=h}4pu+aG*H(G&xvfX21jEi9fqlZSbiLXs8$?W zXzM5Gq+9ZoF@a?yu*z8)Ky-V4w4>MpCg^HKd|5R8Od*tUow0~ zZ%B$Hjw~b_ud`7Xu0$1>S0bXc!DK`dm1vAm^n?p6Jx`gtxKzuTa_ymc>2cm`&zse> z$=zD)FxMt}ka|zk!i}t$)qEqrQ^VSyRq-igTC6I4bTPsJ05X*6>gVhMsr%v#QniFp zli8VMRl7}Z0n&g)G6WCjTJU7f^r7F-rSE$A!<)*23H9=j>kNY6Ghyza7kM$IxdrUe z+OFwe0m3;X;r#4<>Few35xeZ@Pd}bs4BWzr43F(B)Un_%j4{EzW4Cjs&$fA!WlgcM z3dt;zpA*S@lEgFgnL;IuF(F-lc90_1+_j(WWOm|GgAMEv1s9bxAZWn7>yHt6>Rxw$`z8 z!VSsoUomUucya$BEq^Yc+lY`1Wnbx(b!Jzp-C_I5p-WcM^qnf{A+{dfLOo6%t|x{~ zK}F-Fejqxm=>W4AWKYCK!u=? z^AhyG(hIcMMnjR8#OKP(+uuLLVfY~g(C%x-d%ngni||^jpco`9e=#ikXS0h7!EUWVwYvQ<{^y~lWLZVhvSWAN zW$W=mD{}r`p2<;{hwRgEDYGLGIEWsEPUZlPzI2EW{$W`-`|D91J!HGUwZYWl;r9G6 z`*cVOG?PnHYV*X%sq|w_W%`6+kpjttrH_92W>6n!%jDJw`+L!^*e`JyHhQ)@`AiMi zoCpsQSx=s3b8vCMI12{!*WH=|X~qz&e)7(r)hyhb;sWbqdf!&nSuP1BfPi$tCz8Uf zn6iRS*GILzH{_vqpfPz06lUhlpc}ONhtMx(=xYw#6Z*y;%m5{dSry3x55RJLNeS_Q zrS$0JGL(|+8?n$Vu2~T!Vv;;m2yDVRP>NYHvP_|g2r!uFNY;7^HIdAp?EU+0lqq_|{UGvp)!X8;pbbT|46=9_g=D8JC}wfU&B)ers%XI5W{|D?xS73IOULajq#jM8j zq~Kb(&?15S9@&Knk0q0_Ga%SBCioeMJm27AqD(2t{F<+ENV*j5eA6%o1UdiAz9=?Z;!%9udYMVRiNQtq;(*`S__DOH81W zUK3kM?#`hT@P9 zn|Y8*Zng4!e;lA0Osyz$e2~1#&J+#4-L58|)fw zF7gdjLggDjOwrAYY)BY%F?rwLKid2Hwj6V19Igvm4fd(-{!_;CE!X|a1t6%O{}ZVl zLN)g2hyKj|MXmP@HZI9PRVG#p_wtww>E@{M!WEk)Rh?8&1e-kD(g!0Qzap*Nuympk zH^iU}`lhY-_6h04L;`0T24GMS_k=e?&6S3FGnQkrE=Gace#oL-OzJM611CC`Sp|RX zUsu=K7mkp{NYknk)2gLYFLg?~2!&ra$<2=u26}2hL^nU@hqMbFwUiXq^E4HCPNYQN z#X*AYYI2$m;S=W18)m{)?0nCr&`MGewliX-u(;vIy3dQA7hog>?>OzD|M+L5#>9mE zL`dMwtSe)rcf0$c#%hzWNE8*mVQ2oD0s@E!T@0CUM#tCSV5QT*pWmN&?ly@J`c*UZ zMUHj8)z>GFi-bgUDjEg5ywHZ)8TwtwyPAk=VRi5Mo%HEA9CfWEIqh{E{XN`j=gSY- z+8vI%`{~=__+if})nge;t^<>E`aXVGcDsUk3r*wfzFw4_C=Ii>7~g`)mS`s?hR1!{ zEuB>~(Gx{gMWu|PEY7}yYEYmQIo^=h9QIzwSPQ)`g?&Vh>qutXfTOt91yFOS#Y|*- z8yp_9KOT>QxwwrpQHY?Yc;IYDQuixGg0L0<=shtc=f%d3^g-# z(n_h4WUNw8cKzX|XQQJ346Ezm#GFaRrefoTu2SxqkXB7$(e;Hrb>9eWGMt4A7xiz! zYe2uEqFBg>bz;Q9T_7+hsOuer@UL;=KKp|KYNrN{#ov#_ecR8mc37xBbAJ=b2G_s3 zH6;2}fJMrU`BuoR)B^~H!%X8BnIX5rEhiV%7|G@xy#3ND?jdTeSg?;BDzposCArr1 z+>}9_XuA^Ar*DgCNC@bZ(tyJ$I$;`)v@j+}li}+@NH%#X?7+zgOMSy`2VQS;S4YGX z2W=D=TKoh7r~@oRsZUx#iEq1eMoNFWiI2N4f4!?QD}PP!;%YZ0aN5D|*@ce@^x6n= zs?BM4WhA@4=?8Q(+>1|o-FRehEriLpBe>ne^8=FB_Lapvp{ zmNXIC2J;+}k{?bLx3HHrUZIqPIOEdAvZELs|EqF|vANfLkLlLAR~F&)B-()^FWeO~ z@G4Rx#aHyos(I{j|S!D*WH0^k;!0%Wu{`fDDlL71kCg3(MHo~fGB)|6#e zj`uoD)XtsY$An;>781(fG#OSCAeLYw(V3dJ`P?-FA&-a3_1$6@W?vy3egD07sPjJn zdluFoRf=YIdn8ks%>3k5{!?cYGfty;5b@H1$jUVqF3m$T&{c8DW7R@anv7UVG z(2PunMVoWWhvb3yQ1<-WJ^s%`Q&ftHgFE@Km+YI)ojis!o;L;^dEWDANNFWY58J9~ zIaX~1wY|MvmxF{5ozasfNxkEt#qwpR_&pPVsOdSPP}geTd*PEpli|R-3)_}F;a$J8 ze!$pbJe>CXlB39b3SM`SSn(az&P*+tq9c1hO=CF8IYg5rrXuKCt{64G>P|3)6J7Lu zbCLIu#T_n>n!rc2W^Z!CcD#Bk4f849p2LcF_4adI2?+@_cxMZ-KZ5>-?BWM_*{G`O zq{5IqIv-Vvf{C>Dv$w9TpS?H24%IYqXVa=m!`A=TtYlg{y3N&E9{*l97+uO5`+Sy7 zMeJX*C!luV-Yk%i|MG@;ryUI47;AI(Q+6PE|~R zPtO|PwH9pRFjJrqzAsMsInCg#+uTUHKo|4(@Apzh$6FaD89PKG4u1#w%jt$}v*}Hn zZ6WobT}w#e3q0$hpLpRLMQ*d@w3=X}C50l+klugWOgmpkyNtY!Wo!@MS9qqoaHo6d z*BEr$FJ-@VNa3{}e9$%F4;Yn%Cl5+1T|O;~Z)UzBsl=3-g!ZSrOqn zSBrny9{lYzhSZ?YTA9fX12xI=jt*)hJkVU`{73IkvM?!43J1zPn=mXOi)urQmXo;OQ`R^07|LFvKV5 zo7>ptzuxXI`?vwq9riqx8O!Kmbu|}3GWsCF8xcJ?0u*VVaTTq~! zqopaTHQ=ZCcLkPMfx~YNg^846V-^lI0e}3+6}gNyN36eNU~+2nkgD$=o<<)^sK%>f zh6>E8=F|?CS}iYnMu3~|3H;SC?>4*@nxb3vO&r|NIfodlug;fUn<#pNcV3f_GNoOx zz&A6OIs8pDh>8Q``m8rLk*5%8c=mL#r9hZs6nvEQI5;niXmqTahu}l1d*m zjDC=14bRFjF1p&)Qsj`Z^Bpb1E)AROv0%~X6C$)HhhgeSHLBrRbPtgSkAD5no z4NGp8>iq&&Dt^uZ0E*wTK8ZcZYgM~)fon6OJ5pw;jy;Z*`OjjPM$$jeUuni z6qFy$Wfa1A%$6px2h%Z_WiYI}K>VtA)v{AwYp7Xw0sgNrLSBOY>;VB^WIgCqz}+5u zK4TlF{@YtepY5%$zb$cm8D+~;edsqz{u=yph@tiPFS_Rm?{cf2e)TF%eAjPX=lXLj z!1~flxGoZ2UVO?@%`Vd9-S75wP{8tz2wBF0!_C?%2=+0>8_JtSA+x`VE zola@405dWaGg6cS5|jm^ElYqtN-NX-lP00iB!&!{aXyNTB;nhI9H-z-RFDSU{yI$6 z%|a?KHHdIj+i`e1nJgj}$7)QqNm^{di?IwOym82J1qM=3S2g8b{5{+tD4|R$X^$w} zp;=IsS5ly0L(X@2)5?TK+HcH3g_u{isp2DJDQZQ_y05u1P=uX$x8iZAZ<(9t1OU7;=` zf!5(B)KUzQ=dWgpGXT|N-V+ZmU8ko$C?kiZ;Fd&;+nR|v^sr14Q|o{ylkGE z=|Om^C=bVTTDDLBQp=jrY{#CLGppT~cskR+MBJ$+(Vl)*t*=M>Ju|YH7V-NteVt)7 zQ`&e~C_PFbv2b!xnSsxlY{!q>P}y+V!Gr+!=~dZkR($&eg$l(83%CHTleEaJuAU{& zD?fFUu;Lzd_m>E8WVRV^<42g9r;m@K>Mo&ceoC=NVbu;P@J;Rf^Z9JQ!Qgd7!P(3z^%pMy|F1r)frB@z6Q3ApbFX3N{x%JPsp5FItsdYU@~q=o)it9)ki{r zw0U{sCgS)5%uQ?a(Z9a23fOprAl3KKm}g7emncfiF~eEQ(+39sV|m>uN}LF=bG@E3 z#5Z;s-6z{~eflV*Gl$KZW_8|%`-?gU88+a8KFck`!W199QBcBn<0~^ZgqApphZwqm z%~*i6h$4G@u0Kct&Jff6fRz8E>2Hzh`Cn0K{!Q^fki}x~0ujomuQmwowYN4(NPBXb zArR{bikPNJd}T&Ss%BQDnI%IhUGTl>@8R)-R*}x_+5W!WxYS!|sMqSbGV@t@xssBS zsogBK%fGbiKg(>opuj+D&}pvFz~!!cOOK3L^XrAPGmIB835jMQ%zS%52+O88u&x2; zC?!#r1bBqnD)JxR<0189erdYzZM`y}kQ}PdQTe^Tw8bLIZpF|DvSJXQ{E6cd)51q= z$ZOkptD`c#g9{UKR6lxsm~W&CSi~9&`0ereMEdKny%V>PL(az*2hWnbiQ+mLrhgn{ z@fGi?zz~`8rtVyXw)Y<{gSYg3($~)Hbg!&?*3RNL9tk1Xpx`rdRbP6-pdw?gy?zGy zeitYWIzH~o38ZRXfqN1j6X!qs3%iWPVf(AF3VM#=k2l4Wj+OoB8T7Zd#I7x31{BFu zJ?}bRlta)=Ay!{-TDofWn)66^Iab@}z8W3BY#d$nmW!qE7KjrxF~G}7-1EOsUp*!B z)XwqUAO83Da{o+VFaZlq-UOa&Lw_RLkN6ag()0XfpQ3uQ2jz_a@Xi#lnc%U3%E|hC z{)oeLd&nyLsty;o`g7Cnzm&7>cz*MP)H02*n)#yVQg@a`z{`Do zj@isp*X6-Q>ceNXGB61E{9z=-5zZdezrsd{bgSNTe4Z#E5JP9~mV-dWJ#B<0x+`ZT z8=|8JHoow_Dd|oI0`LIZz(|EnIeOp;s;e31<43f!zPD-Y3#K%b3f-nd>yXVn!jF3n zO=@ispQZ&J1w-zW#sanYzLZd$mzUb_OlXTd7rScH+BsV2X4(seukIT!q-T;x^+xzvIVY`TlWr2B0 zN>@5ARF!2rm67$eoMqe}j47e4#c%asN6_@#Qe;st<3H*|;Ru*d==rzz$@Wv_C7(A} zdc|U?002+9e=6J-yD7k$45-?Ot`i(hAjI_;UW-qS!>Do#^^6<#_ES^3F2`S95P){~IE8M`1X!=DaBkr4S z)<09c=_|ZlAvUg1`gNpba^Yf8Amnq5thkU&AJlI2?xj-HqP+^xZVG;7$U&AeYU5;Y znSHq<_{E@#*^cP5E4S$)-?4Nm8y+yeZp>yiR+OIvnxpVyU% z!*_=I4&7#yl+UW(?uBzW*c#Pa$Lf=B>KsW2MTN7!@xwt zjEPpA!UH&SevaGq?@ID70d8mh%dtAH6XS*a=)`nnG6lxYM{O=2=n;!9K@|D0kn_kl z<2IBl!>3ufSN|4ilTwqDZag{!SJ;SwKdu+EMQ<17oSKIrp`vc2UZo8DN6 zHWtHSLPFc09Vc&S?;f2@BKsndWR$`ibaedOhy9W-)iktEJKrX><`03CGt1_#zIc5C zx!=5e+hAOCGc#5zMi!Yz?aPby^YUJV^1lS_t_vU__Z-0#E0gJ+X^=-YMD9B zVdJLPw}=Eno6k^|o}nf=eMDSCU60jdd9O*m7m~1m@gBRzvO5M+1hBfrvby{LiP>XS zl^4K&WjA@%F86R!&m4T$A;;86+A}F|L3ZGIzX++1d0X|ac?@8{j#ym{v&0L_)YhQ} zlCiaa))o1Xz{=+oDbgr`rg+`(uEcF?d2ta1+>N$5snoNSxb?Mc!>L5Pe_WHH)2To2 zrD+kFDaPb@n?9JKV2P%g3oS{TCW=oa>^cR z+K~Q_7CEto@A)A~-mYI>d2(2cgxTQJMVA)+5M9&Wis66Km{W;S=PiM<$;NyeeB5-2 zY|0+Dv6NQxaoE|n^y;siQ&kwJB>>BDVVwMtH4}W`3W9_NR5`t|gsR$*7j(aO?}ZH- z%*Q|cLZU6;9D0N1AdqHmDYh~xe0T6jguS5WypB@XPD_7bQsL4WOxs|0zO9aT!S9hD z-3ViW!ziL!55M$8+|JdSF0^UO?Lq9Swg?2@_lj4+YAfdKup4e zXuJRZe%l3ufnN}b=i%2y?mfdWcBnq&fPbOEaZ|g8^ku8D}QkzdN8rw%jFD-1nL#<<3^O2OfJ3W`Xo^)^>dKI1BH-wb$U9qrlv-WlRv4n8A^i(?P-t- z5)@QQ(^6NnAnzeXyLEBuV+Ra6OFON22M@V{L5%iI_wSSrQDFxn?rniMX-QAb-Wi>u zOfl!rN$xt;dRr=GO=1N$e~eD=u3q3Vu0NkUdj2~63t5uwa7qpoL){?Ip|5-rhigly zq0OnGsd2i-Zyrdk_6ySbgNn4$BMlXSjUMbQGG)VvPpo1G`+q@L)4gMRg4D@bZsUnA z!MyT*M5cporOf!|;X#qbiQZ*Sr7jL8Ir`O>7iwi5!OBDWUoUUysqKT(0XAx@h=lY(bX{JTX4T(5sB zHy)5eFOYl%!!LC^Ml?;#=+@nhV=aCQCZh4v40p81Ksp8*u87r9(^_%*hrw*Z`(!*{Y$V%O~_*POQb1SkSRQeOd`(JEud2M4MM@%pBJ_pGb;f`5}V^`^yC z=dw%AHd2GLH>jQH3<6~7KV6)M0~XeAdp|-wBM#eV7A|%b95&Z8XR;3~-h&E-20sc3Z0scSJ0yY9V7D>@f^wVS+SD z_HrfsJ*~I_N8c4!?s9H}X$BMCFF8&WqZ*a$a{P;#!ET%dSXR~}1ymK5OZuqs-KcR0 zCuYr_Wio9`@XX0l@52dpe?oZ6G8RZ?D5{p#+zn|VM4nJE@2hS{RjXu4{Z21?|LxrN z>vck5h6zU-7jJN*9uM;58$3}mrAaa+>yujfM&o4esSnPYazJoT>8f_^S@5sffU-Mr0qqXA^tIfO-kJ)o} zve)*3?aFH$J%w#=*4GD-ZJ+kyh3 zmQnZID1>h$pp&L7-zdOsGyzaMQD0SMV8X3=A6{M%OtqX@pw5}}p+DS$Vw;%WFV7@2 zJSV#x6Di;B&$f9HLK|^}Uv7T>HLMpq>H!V*8bm6ecrp6!9C+IPOV9&ZApf|F&M8mI$jYh z4E|6QdH{4fRq8>-azgPHIVR&FK|m%aZT@0e%Iw_WEDiKIds3F)Dbs)a<)(U6%6TgH zq6{Aau2Zrdn?rIhwD}o+y)hL*UgOb4N$RM))Ms7+hv&|JFA!K~J7=)=XDG z?Wq3?fBuX9M>S5Q@6mYz5qaQgcztuz??xj%PX%;M7>0lbb_`;a=U1^GfZ4sNbvW3Dj%EtDAL#eq-iMMZg~$kEK^}iA+&ZQSesNB5N4b z={s(VTbwxK0I!;wV7QL(2fMFmNJ7cm%fim?Ctm+qr%tpSulM!0)|0OUJIvx{FWGYO z^Hlu~QN5DFU1FORkb@MI0YtpRmpQJp5$WbUAr`h!=@c=N#+y54%~J?V|9|gA6f%bc2+{5Yioz5+foKQc6g7!vI5fmox~7 zAV_zoG)OnnE#2RFzw6@1{AA9YbM~|MUh7_Z;d)Hzlm4!I>e66`7`ju|Kxlc8a_!$- z!l7nyzRQ76RsCj819HX*nu1wJ8^s$&`_NN_x=3cg^sP3gj|fEejohpi6^Wm=IaqGsng=eOL&4^)M*5|ISix~=x|T^C|P%4=cxVoe>#1A5;v zPdpF*x@BW&<1vL8nMQ+7YLpA(SFEdsSLdfq3hGVW4pYav$V5kvMera_NT0e%zt9dY zC}6732uw6V&oM=LEQx>SbuiEIXy7-AH4e<;$*G@j!gu*(262j1vm(DRz5MudX05?C zUJWUtnu>_3EiVQdnL-n6$UV6I1BBz%yc#Y|{V3OlXO!Q4#GUniB9#T+C_~=p)LzXw zFcw+GlIYZ6d7FgI5St!QI2FIZ5-zVTs0v_&0o@eDot8_^qVAdgxKDJ`_nq}oh89by#4$n z0WWUb8>Co|E`DAV1W;Dhe5e!T3#rsqJ`X>p8To{Xr9M46P# za)=Rj;CF_eOjiGGa|OIq&sFg89utVy*8D&>`V+x8u5eu0@hr24!rDE3zDik1=I5KR^GOfEgu>8+c?{ z=|7(3jG2;(2l_-WQ!gAhQ@7rp2s-CaU*{V+#BDT$kSYgp;|=W?3fLTJ5f5VAL6DK- zYdrTGKk+wmCB?n_Tt%(#XpM+Ki1K|0z57%}lyuk55_5lYvXU81#iCJEiy5E~AdYx( zhx)D7ec<2TEBX?6x4hKc^z@fMJrUm?tBqT`EX4mRhKak`*gas=n;fTHhGCuB%;iI3jQO{p$Gez^=#@d`4*2gFIii&O$sd~c(SD{=R!_c3=%D%`E-@@$_P z+kWB)c|%o5xI1KHxVnGmtJHZMqaE*~R*=ynvH|$XEGr{|GVdoPS!s+GGsQ-(mM`!Y$+v-LMqTXGJda!gmA%DaQPmJ z6hTK3AM*aOUk}*LZP-HMwGF_F%6yQ5NlRxfQ97i~8@b~P{VO-=)%>C+%DaQst|uQ9 zfeOpAwFa)pwHLuRB4%P4+$6Ew#Oo1Xt3PCXZJ+vty~WG`%W73-2Zj9*s!i<{8d85e4*AU zH454qP7C3zO~+#U>k76PtN#3}r(?s5RzMj`BXZU!8uR1^cNIkP)s@7k>k5*}ouZ6; zes$F+M-HV6S&podj3*MPO|?qoSUL+U*o+BV{V!CtUqKK z*sahfDAfhE@Z^M!Q-z=E<7Z}O#wrW6-{+2xR~}DM;u1hF7e6b5&G&wV0@$RU#e*m5=EvS6O*ZA;($=8xUPe}JsIW%6lmt{ ziK+U?n?ksTa`_5Y*uk0TVz>%dJs^JIQBXWs`twH`(Durb?U-BT*Id1%_mlMd?a_Rr zoq=7_O%8^6m=?F&IODB;1HXl$21$bM9x3VCZke-=k#9!;txU_9*HIa z+j*68l{ccHm%WKMozLu(vD^5%DEDqJ6yi+Y?iLmK2L*QdCHKB@Z3|x(2e;3~QMm*o z+VLy6#e6jE+hx`?GyPm5JWsc*)5U-}Zvtpdm zD{v#TCWQ}IPEBJKeO`~AeCW!<}^-!d^-cIY!*2jnO z?kqTn1T1|6FZrrrs^{y^oc1lBL#tN5d=`ba_ZY?Zr}0PZT9d*i>$XkzrTH1_a=1Np zkher#r_0xUY4>K0ih{iHBvl#j-!CTeo|6??SnF=VOw2IDebwK@CGPaLUx@wRb><`I zjFxzpTQ236-JSP4sOn%&h#IL(`J|`$gXrSgPv?FhGKRB6ZiF))F z;1A82ymna@$jGhx>9$z@P%n%aNP~R)%v(qu(L>oPjp+$rmEA_Y*o=%uW8=_ZCFvp7c{&Kve-ph{FT=U6MUb-X3QW@S4p#h_354YcH_?FrIkA89bxs#H?A0yl(a z9WsXd_G`;iT1=moD$VR?eYNvKLU&%rn^fqq>k&dfx`n8xh9<1Fv%)RN1Eci{90;G+ zSwV%jm21%E^httmEMjNLs|8%{O6x4&7Ok!M-FKylSxyYDeQ5J7ep#=_`Z4MjMiy=*MTgXCGTZn{L%wj?a2(qB1^8R z>RyAkq^>gd=o!NcnuMWWA<`gI>q0%{4v-%Ur0b6|_Cb?*6|LV{hybLAkbV=5XkRIJc<1#~~wO11Mzqw6a*RHY3_j{@R@l1z2jLAIZkXAE{ z&vtfqdr0(bOT0Wi+jOiV9~%60B^94}@8^gn%q%t@+tj3-!_Z2NS(nFz8SGorN=G!i7dKuJ$ zd=L7@vUJN?_eaIc_@lP?!P>LX)kZudfx4BA@272ydA@XD7ck>cVbb$mNoGuQ7hssg z_s~?c-*w;EGiOKxqEJwzWtrVPa6kE77lqKsuVT$wHgWduySCE1jP~!;YMd z2tCqVuW>`qlehL>`T5-G1&3ZUq>lD}5S0Cs`u_0mid&@9OQS_9sTm@5qnl=rouSPN zclYZn;nAjgh_|T~y-3Y;u@}wvIct1qCeErpcJq(WBd-{q>@w14ZFi5J1YQ8XYr>Sr z6ceFBJ>znBhq+Gf17fD&wz)hvWuZ1biZbrrZ6!ec)g!HegD-XN{HVx&`jy9wx-tef zWeeQ@JQ}<8k;!8`uC2)`Av?rP`537p#rz!8cx?)Dt1#tbysC|YzX+ZV|VMS=5 zNbEx55qX*L8*%ZL{_V4Y*eIVIp>M8XuCCBG-^!Y*e&A};6KW{aB`J4VRK-iJ_@m&= zJ=V=EyYqnpXfa=~{l(7O4uh8@s*(rfp^`!pwTfM};*^SVqaKqtL}*7r{ydR~wP~VR zb*H#BZZs97zL%wvbYQgl=rgjahAc(zLM`@pK9Y!I!TBcMuf$5}RVbl!9n8AtOz}rb z@!B;7ZawjZiiQo2oBl)IbWopBl_*0iTiN+~5l{^Hvl)bMlnwjzk=oPqnW%c2$^Gj< zk}`cIZt5A7bV6GjoSog-fp=0l9thRtz+ z2emf0Fm!8lJns!H_hE8RHsmX4RVR^e57G^8PmR`lc1oFlVaTa&;AJ&JX8O)w$_uqw zRw6s*Z;h$5XqVieOd%ZFV2u}i^WLaYEyPnp5v6bGdVCR!+7kj%l{ttzxmMUyyt>~R zxtuv}dimcKXooSLchNSmT_wQ3<0t?AT;uphd8c@1p1m<$XiL$ZN&ETh&QWC>=Z%2~ zz22aiALK(8`J;Tneub2tC%;PV+S`0g9x72X0;2SwCr!Kj=zPhj4Fe@pcUReB9!KSG zzbzhv9R|L?EA>5K4KiNoi+OsydfwK&omG+gJNN6b$-gfAz@t{m1@4c{AGcrif{?t3 zQoujr@9+DG!SlP0aWB)uXMP;D#bsQhXxZNuT!><&3Fz=WP97%0E+O%vavXZl_MRxl zZy;$#r%+GYmMoYho*+%R5Rz9f8V4cD*J20$0{uf!^ws3JdBCsvYO^H0nr+ugf5|Sp zh`sLZWw0lvLfm5WW6PD(p|nl6cZAT^jjT{tAD4sBqPHtho+P`rF16-m^@*+JQ5>m! zbCaa2Gi5>hh)^MV>p!~ru>hrs%RlXELPQKwv505q0vx)G7%F4j5|wdWkN5W%zyCb8 zo^e=yB_wvAZA|71+YpN2i%xheWp=YlxAVRYb;;}4Gy6DGfo4Uy(c@X*Rhf1vaN{zv zb4FC4E!?-T3HI0oH8HPb`J8ZE=g;Yer}-sn!Fun9$((+UV>WnK9zGDC$pZ=?-Ii8d z=wZt5Ex_KuFqNY`68C#kqePp&P?t$jKR3}+ai0La7jMD(-s34*&n|2vntk?!3Y2Xs zaK(Gp0`fy?KTzcgRAmohj28Wz+2*1z`0a{pV zxcpmFk|ISrx-E-p^t=hvuI(G&vE64uFPHD;Uk3@k03ywcMkd()hQ~b3f#g;Xr`!wdDMfJ$6 z-M_smgvYx(1u2Dz3VcT{+@R^}WZ8>y6)59<2VBw5D# z6198_t>s@6N@;YXjP>G0roI?p$_(6U-2P^7b#NUd1u!4=XWr3$#ntc=NRc1(A~O1O zHUBd@zpC8{w9F%MWwA)r9_Dov7IC#isyCb!rJoDB9eqMNt@w=di&r>0JEv_6(H;+O zzrXBe%-c5(p>{zX?9p!yq?OCr!08D{I#;{?JBE2<^M-XJtQxu6m73i1bHhf`QeK$d zqzbyu5?z|*Xh52aH@`gD-w&SHC643?A%f*=adlOGd7Z4Oqv#Bn{5A`XAbvrik4fl3 zpo-1r4;wDOKUNh^TB@e;J_kAnA3n38SU&R+Ipzdu_RP*7AF6=o3e;PoyN~laBqR)_ zc_Y+i9|h~622iZkU$tU&1h;`#_ko*l6CRmg1ZMhN#7oqIAQ7si8!u3|g1$x5b;k&c z8~&`xp3)I%lR6wg2cbu{C(Sh9Ca;$A*!d&tq|^+~_UG}I%Li@WY#429fq)mqm%DJA zTNQ49khQ2AS2Bme?F_YcnWw{D(#x4UF=e(NG*rPni_gFKJjd1~+o45lduYzi79@vd z_(?hSQgP~|xU$DGy0g{QfZj}n1D7Pg0v92gsL@Ln=tfg-x8Sw6zYnaNdsLa7{`;E3 zUmkk;UZ(u+|HKPAAu>CYD7#?Id%XDesll2A=#zGK4!nOA6%`p{y!%l&!8Y_&m}pmZ z{_~dA#N5Fe&+=LE7 z1F7~TUfDJt?Z8cbw6ftR44zGqJYS{r)gH4fB3 zNrt23&%P08!M1YFeIdaZZy8blu$;Nag649 z$g?1rsTU%Kyvq_zj1%~BO9Ge}2&bn}%4c<}6*9<-ZI=yK@PUKLxMo=ZqJgQqgk5C% z&rFNM5HU$IhaW#tQMzxRVEX#U2FG{I{+Y}p<94k;@2?{+$^h=ssa%*5^dvkcOM7J8~=($ zgb|ovr}}||Gb8^p8+~so#d=$$>Q7+8g@_^26pRBwOjiP9EJT-my^a!v26HRpR(Ll8 zndAWM?2C;B%>Q^EtcCK*R8Rw*f{f4tKBEtrdi1ORtq@)p(6^V&8`1EY0(&&v2Cg8! zNvJfovfLC}ufyTmc5qeHG)nVF(Mu-LgM6x_WaW=X^&@}%{ry=#=>xdV%lznh|h5tI``Jz`x;?lVphy8Hdr+_Hc71f`6JBlbIWgCxk-{L z?1VY7JEaVo;iq`CL&l%%yI8l*MV_{P{n;fn&2dkUrji5?(+&tb!2{IDuMO?E!OjH0 zk(m*Lp@8w`LOWu2IJ`j&a$O2=^2G5A9fb)MEVFZ^e%q=;6^uBP%s%(MsTz?*FJXdE zy0hW`iN=GO>K0#+k2aq$KoKhC&6l|KOaiDTU4M2?MW`tFIzV`{6>i&{yP#;%nOwo4oMJtLtrYG1AoS6J|tJDi$N)&Lg{l^6(bG-xo<;%YT(b>czLO6Wa9dvM|wG> zaVQ7D33ESigj6$q*VG)Vy%Sm^3gz5_Mq+=2%DV%ytWUqlVHp@`ElUeR(ScJhg$ifB zOeDZ00uzfB--80a1)g#hhqBn)Ea0_AgKXsOUA6n08H0xCe?IxFmhFK;!MT?xGLle( zwMJXBiFSZFX5)FM?QlDN-zI-vB^+wHR%n$iD<>Kqn>?6-NI~&7tPa;}4LEGMQ;U^p zzY46-E?p{@_4!1$^N^^e5FHh@^p5o}1T>T(?$gY@xnJv+?aU8C5onZN6%AX}BTRA~ zj{!REG(K}cYV8SHg85Y49D~TDa~%4crVk-xrc$UH%A5>&R$)6gGCZAFWYHEK83q~G zSp%s)MVpU(oGa6nYp^Me!ODp*pOj?(HT>Z1UF}pU_cfS7rt$o{z!X|_Ap7+*fL|P6@0>}RKdGJ5EJv$zD2;EmQH>Pp z^A&yg4E}6FQ|NE##zJ(%sI90}2y>&t8rsmkF9QWvttVaV329hr2=)kGRP8C z3W9}$fyVPf$w0yFej(DJh-Tx>JJu@_!x^ton~iKHkFwX=W#;bI*5)AdZAW3qIlhjC zEmMKNoD$1cidF3@LESwPF)c+F9JW(LJb|hrqgRp)NBtp{#!p6uc}8tq{3_JpTJ-Vw zQ+d;E?_zh<(umlmj`vONu@qr4#cBxMyfzya?xY3WQ(>kR7}GN{GPf1iNL>`xv-VN?FW*82_^s zrLFYoyI&HxTaWaZ8xCM8WSqf3W{AY^wb5RO=KeQA1W42P_#G54C*CweF!)k5wU6J z?t5)n=ldW^CtVI4mWPDO$1Ui<>8u0wDRz*2ZOSf2peE;b(%?#5%2oqPajT2HKV(!?)jLBZn+DX+F_IXGQfsN#82}H0gc;R? z*Uhg@_uI50l&xbcQ#DN3Gfatxftc!+6dQ?p4F3Hau>D;BUZu@@Zruc$53d*kbc+e3 z{OLMkjaY?PH@_9WTZy!dEdOrYV_KTCkEu2--BnQ;U#u!nIm8Ol_zx7^6zo;KiR%Kz<1lNy-L5YIT=v#HT zlAhJf>+pj=g|u3Cll*QJ?9k`gRjE=#hI%gT5fQ7%6GzAc$L=jD`fh)@bMcuEbtDyM z(xPDZIFA=op+XnS?M0WKiKH0GraCK*#CD1@JKCTg?@yKb@d^^DK*68Ms#+06@+2&L`u1&=Q+1o|r_ zfW}-wK62m>$uh0$h08Vydib$}A$(}>mef*Z9H!#!PS!<8DU@l$m4mnprvAhA+GZuc(lFltZhgXfy znPdj^{SgG(+oS(3&&b;Ai0E09RD`w+LL#p=7k4;G-dgepD74>GN~6E{q}Rduy?0ih z8hzA-$7)(NFBZCtAY=X(6n17 z-obPMy$7~IuUG8CU43j`v++p?Ac};){}M2< z0BGZ_CmS6vn%L?Psc&}2rJgbn~{kUHxg{_-7_U8}o=ov9I(=_$&U3J5~ z-Q}iF8*n|^a19^lXHM!QAJFV80>{nTRfLOOwRtQDKS3Z6qucY{R6##J)igd({1nQn zK7p78=x+T@CGH+b!r`>%y4&x8^pZ1a72E$6NSTDuL7zg$o)C2Wj%L4qLp{uvURcgK zOv};L&XC)2?pvaTes`5RG5QA5w4nClfj3o$8J2n!q zLdFU`{N4DtDVkmJBP0AP%=h;Y{%;GnS7k_QhlO%^+ZK3<$$YgXfsESpymvrak&T2Y z#cPFL`>E5S+Dy|Bn=Ld*0h|qORWkK-$(cD2kVsLPBg$UUm${&xM#qKM78f&OyO&C* z4x4F~xq>2XBG)i@3TJ~-^e;vwujvqY1WcDC(e}427x{KM`0%hz)BKtYI(vmmjqi3< zUDK&Go6fO$vM2|}vSWhVAFVa8>}NRZRe1T^pZ*HaoeC1Y%D`4VjBimA80_8=Y?of#tmZOyGuyMzpF$ z9xEYknjkCI(|eEfc)0kxT<&^Aj??bS??xW#D+OASY_G zm}LBA+T6?2YkXz}pRk$_fsdjU>ULCPGecb$ZM0$%ioOJ1{W)ii2k6bav=|;p{4aGw zki~2-mQ2QLft9*&^lZEj%ofZ4DnwmW}5Ci)Ps~IV*Z^P-RCEbLWTuTnw znUZu=B512sVxxCw)1tOvyJz1L>FjB|3p&Ol+bOLnr%+S}&3O1SsF!}|a2#rI`8j=q zOWJ_yl#SHDpJ>sW^-GY#X^>jgLi^uOydIl(64k~?>DhtRmq}VZ%gYi%(WYxD7Mq@- zJj;+^>a{A4idumjs=C5s%1Nc32}6d)(@rk$K42_!`n7$DCDZ==4ETU-5r|;nOmh$< zQ<=ugf5Q~AocCyRL4|BTx11dNGlzj1og$;pjILx#raLe+>r_zq`=3hlOH< zVyAX6b)URQg1^?6Aoz$|o6q)=k$}+db-v)=&)zCJZpNME_6#}F{|F) zGsv)S8gKnb>)$+!PvcI2)1(X+0N>SqFwJIr?(_}nW4%dB2;=hgP{D3cogf(=W z{@}h6kY0m8ei?rTo=XC>G3bRZ>t_$jNj9>sqQ@=#tI$!qfH{&*IGM)u>LeJ`!4-~3u(|yDJ+3q*`V%pZOi~~gw zHpv_vfTM3+rR}k%^)Su+$ivm%er{P@+`X?I6rC#a%5B0d!{##DamSH7uP&zY8C z9{x&o>d^`5AaK(Efh*&M0p!`SJ`uX7Nq#A+_n*bHIq9lha2$!qHDQ`H*nAZ9d9SM$%BOHQE!u}on+N% zA#NqOjU7bRul;t+__~j6k~v$#W2->V+)kKL^z(E(r7x{OG1_(V_S)&6($n@?onE5# z^f__ygsnGO&GV+c;V)EtMXTU*e!m%`g-&0NO8PD6kWGK2p|ZZalVx#Ri8ipN zYg^eecQRLaoCeSRJu7wG1yx!}TYWBdQ6vl{FI~MKW*4L<*42VTUiSuvg!tW#VbOfJ zugh3*uO9$vxU#Ys&g!bnTEWuYowJ{F^?{HiIE%)bKVaYQVPD=4D2JWghgJB| z3%*`o8@%4CZUD9HHS?xFE+e>8Hi}1q6qwNg=K9#s+upLq)8R}>>GYqQka&>}kte&7 zJOWbxepeKR<%TwjGQZ)W3?$b|ul07Ov6Jhr&~Mx`d=x3=KkLqn)in8qLS!|&d4pcS zuo1A$NXdCAv)LxYKMAwX*UTrMnMs2?Be=zJSMalE=Jj!mG%Iyv*fU=f1i!&pD}FSc zWl|lgoCOm)=g7x>;{!<5E`jJ5XhNcpC{#q%Xj{SIJ)fCsU$y1i_u8eKD#@INk6sh# z%&Q{_AIWF@7RFb41!JB7%|)u99hX{*6N=j1a+uw<_-u~4y`4bBB)mjRA@}6q$I+3g z;m6AL!o&?J?9-Bf?G`If0-;e3{T;hD`!x|i%3unV5YSlP(0kAF6lyZEP|udHi^W?u zMW1+B@^h>nxN;F+20<^JR}|S>1(=)-`dpD|ZTk&JgSm0^U)>Ly1paP%{nll=^bq7T z%qiHxMXRpa(&A6fP<5tAXa`W{sRA|>do>i=<+>ic(qbi1kLVz$DeR)vay!A0;Myfe zn#cRG{IU5QlBwh2`VxH6=vys?n!Ov`m2c7WOmQIugwi=S)Joy5_B!$3L4ZHtcQRK` ze}6v@d_oYkSXZk*xBemm8oYJ}>}`%k*6kPlo*u53*`5TeuiB!od!KI7XG|&iQLhjG zZC+1&e|G_0dHT!=F-#XQ$D4-W1r*$Cb0yuLPN}}up4i`-3Zd~31NG{(s40g#g<2Gq zA8`S?;oo7z3)O1w%ugff*;HCle-^`_nl;#0_flriPo=_Bgr5=KSy-WV>0~>301Y(*+URms=1D=jN3oYF)GHj)*j3#9Bu6F| z9$(Kwpt&Iy#{OZJ?W}`P6T&dLJ3u&=7@{hIOW*AWX5$*q-6Dz+H)$#FK$cn6c^_V~ z{^->N`N>*kexKP3OCTYBbIQX4kqD6EZDGmts__QWHXZ?|*u!cVP>vJf_b)aZV30-E zebV`n8%e)MQK~}gossKrJZ7VSan#t}o}kyBpLqn^S(t{>?m~_^&zF(lp01rpUCck} z<$FBB@k*JoQEVi)@8#r%kH=Gq%@{ame=TJE^r|iY1Yi$OB&ef;;By?xOH1I+it;EP zD^iW<5B9oD^=wt}@Rvz&AG8F_*xy^$?%W4*X`yc342X>4U*8t%KHw~ckicV^`H90= z-Lx)}P_y?iS4SzWlpb*(IVm3%7S&Xa@Z)LpBO>*xGAWNoHkC0FSkzIV z!%!LTMTf5+%Wj?oPe@?)XWZvd2r%JtCB26qRxAlrtp%-J93H5hMyDLecZUQU+*0FxYw? zt*?D#!ucr!iPS8!NoO4vH~|Jir14+J21#&UMq#39j|>l#Jgni_hV(Bo7!!j+xAo*G z43ZlaO`IJL>!nf*z?)oAZt5rb0yUKwSx_-`xy-Y5V| z%fe(rGqh+Zl9EX2`tgA;ZupaU*Veqa>%s*=A#nl0zL0-r?GmaC7$8>O)Oc=~>h5Qo z|HlWzP@$s?2Fv1HM4La?O$v#Z{uL_4l=3?tnEDpbE3=7RZ?^%R{KLx6J0P_4X*f7U z!TOapki?dqHT-#eVPWAHP7ZlAc6hKKD>O(>^_D?*%1EBCZhJcIX{To(CXYvpMdlk% zhG?2c$v8z3mET`?!}PiW1N-TcSeZ^3)=)iBWMd-lUXK--0QLmmH+wWtna$7EIfiP2 zbrYUxGJDamoelWl@EatD*yhsq52wetI8D|UDbLu-KW`NQHbmYu)qjya5zF7`r`f7S z0#X&=VE@qAUcRC2Ti>zD-)f@kfSE>gIpzAfO};ZoA={oEo)zNR#Z6&a&O|Q)9DW%7 zq$FFGA$ODFzj6lO^gczS$jE>desxvP$-4-~v^YV3T zWldxr(?V@cdjE@B8&vsl?ZI!jj^*!@#h{PCd({cEW{exybOi8L1ahJ{{`t@v;B>hk zaN63vKl^$c5chkMMDl1v2W$gjJybfJ?*V!`%8wewLzbKZoj|!ZZm3~oqK|dbEWXT6 zg)j?f0j9{_@P=9=c)vmWC}OAR;`IrVClUsk-XMB2TUv8AD%P`1Yol#Z*2bM@lC!2= zBA`a>yFexNc4bl|M3KfC*qHwpiAUfJMw_|uHS#()uRiV zhKK)7`o*bOyGm%{;Y8{2`usFc=|y>goVhuR*yvlCey7l4b%mXts9^yStQ^_jEzF8T z%y}weqfWq^f+0>p>ZQ*{^eU@|SG&^YepYr3uVgdAoXf)_6Uhl}l#PK@|Vs#6-8h zyp}ep4ujZ4Z~g3Q+4?Hyab@m9{pES5p^cbH1G^cZ zzmGSeVPptZB-2gD4=@<}cDP;U;Tw5qOA^PyXeH({{p0?n3Y-GzcM`bjPX}4k-95(k zO0co801sg*5A+4JKP5$zrOHw>fKkznjncAY@16i(=RVBD4UYibKM1?dIS44Cb5-W_ z2?O7vc#cc82eBff{s8iP`z>7`PXVe8W$l0+PtzgO|HITIc3NyGjZcuMMy%^!@(3+3V zGUJH{py5QxHIzusWW<*Tr36T$0S!A1wre691C($a;JbUQ4z#JSJ_^Pf_>Rx*?Cr>` z-qEMFy$mXDDz{p&TST|*G1OhCR@*1=CHV0+HR9%BJuhcNNPN&iB5rhBfRdaV1N7B> z{=>k{)v}KQ3{9FqitPg5thK`>^Hq}ai!|15UC%&8uxcLvi*WP_HvCOTY~6i8^_-Rd zX1jez=wE%p^!|J^dF{9m!j$wY9vhQlC_A6wi$ab!>HG*S_Dt2So*)`&)JuiLgA=o| z1_#@;Rb?saNWS1$w=ah-aXJ zW>@Ax1GiZq{g|vQZs1cZes3E#y>p2cFbiY5eFcpu`(?)5!9ixGRZ6Y+!L0P{45Tb8 zqS&dja`yEaC|cy`Q-)WeIPA=lZoX8vEDw;7|A5W!2+VJhkhjUHeRm-dEyoEZLG4GE zMMag@3@k*xMg=+Q_nCl0TNUIY=)gsp?nCZ|d38w!KD!tvfuP{wXZgcdY^c{vv4`lW zi$iTQN2@t5=weD(h=Bx_{HjtS-F?OYI(P2&XdXWwp6df*J2W>NtCaZ&by8#0{bx1s1zGwymkizMK(>! zQoD{J(!%D%gTsBa9>pK5k}(RfC?4!pmXcM48*<%>M+g6kIS%UqT$4yCxN0>?Foo|b zx3R!VOy&sE#yx`Ni0m4QhEw?KHnOZc7dybKj7z$iui6&Vp=rd$VCX9kz+H(0>F?aF zu>rw`AbYq9JED6RG$aN-!3EI(t$Ho<^TSI91{@bEjJ}IJ8Dp)O@MWd10wuugkh?&1ml+Fv~lIMWDk%Wn)O0jRfT3Lo2$5a1uWQGANWy?(5~v^Y(n;r!gc#b$GWqA4nDHI(QGx23@!(E$DRK}{r4~g}hi4q;U*rBL zc{I=T6iq_M0=L)fQviT*480Jf|8r!S7W;oKTw)?xZ>l;Vyop?&VgS~KYLcbGeQlGj zdJO!vdxrg_zMZRnn#!2Kdn#*tt}eb<8w*Mi5XE@i8sja2HRA$Bu8r&NXRdSg z0>6$3sF2BQWRV6wNQ3z4OqH|`U{k{9j8O;#-Mqi-ZuF6m8peR-8O-(719=g>Re z)~hQNcSUycGhW^Am(x)DFDB^DzmYjfjzmgjENajwo}Jypp7sJIKrp!@wAZz(^?m0~ zV(;P-yLHKR5mOEcgoRa%Z&eu+EFXDfWvci`v9CuBu)lU-fIZXs%+B^4kc`fzj=&(J zXJ{*o3KN=33tVRqgZIM+v4zV2zp5`{%YA)a@h37P47oh@cw+^CPK zWN9zr1L&+?quy9%KDQg_lViGuhPa}u(Y4BecVOLl@)6B(adE>|HMdT^sXZ^>m*GY9 zcvBL)95r*o%#XZ>maQesYy|N2>{`UP>?&Vv*|ff(@wqfip7e*rLb>4wOIF`0?;r-A`dW1)#M7#$%v0QUXehe!Y1 zT>8jAU*3Y(rP+G;Kqu!!G0_XJXauGA|0}F`8*ZQ-uQ}^lzoW$lAnGiPOYdtF`6S16 zw=qJ1d47$J{80T7kEh8>l$1_^NDrGf$KuyxceRu?*{H-7N0#?Lr5%l*B=GxTm?U?Q zds>F&`um~XqHP;OCt{@590*o5Y7>ECA^n+g`|*<#FpcjuEg20B zO)@;o@TXv_ChK2lFh!kD;Q*8K@i9&Na?^1r?aG-ISo#hc_H!S0@S)lQ`sG4bD+kC; zh!u}H#(HBr5L&ZyNkuFN0>Mv|jlSlm*ZF6~u?O&-&t^BNj@v$p)yw`yWIDnjF+WENX(*#G2KR#5|(IpH^ zu@&A2`{96#1om)|B9Xz(9Oj5bUP}xjZ1=2;cBc*p;<2x@rnT&n+M;lyxqqAP_mzEP z7T2R8B4W_?hg`fkNAbnWByQ&8?Uc3_K;5$8WcVmCB6g8UO0-5)qr0Tn=v=b-O|I&Z z4jKCW;Zh$CMjD6qWxJ!k)c!&C4xR;By^~>Ed|0XrL(>7~9%M8WuUmBxcoze7-X;0E zZ-%qP`v|>r!j2fFl|GVAO7-BRq!VZ5Bqys=(er1@ri-(YDxr`g66opa#mLAAUU=cf-L=*qsMi}83Mod+TB4utJa8Y}`NN;0qoZ2$TvL)Te|v8p zZb?<055H^gsycPXI}d$(?%U7|&5Q%!fFd%)8AL!45jC2`82yqwiOEkBqtTc-@riMW zaUSA~qB!AzD54@X&_K`g+;{4G?m4Hb_Fmr~d#JiZ6KQVKYR-C|*5>xP=hUg%d+oK} z^{#h4YI{s0fNNj+N@!MK)&dZ31Mnwfi^ecMJ`OluHRWTPUBADRV=l^hlN}o2(g7m` zqP>?jAR+rQ;Jk0lPFQmO&s{3vDhn=Uc=*ULLpinTv{r>Ukb$}O%!pSABHN&nq1A39 zO%t@*Z6qBNMPym(orhMgj;%F7?JUcVXsxHT=7T{X_iD|%(=^$owcaI!*rzogR9a0c zrDrB4C)+~<14?P_nP|?DCbrDvzLyJ$zw?d%U61}(&EEa~k3g9X{>Mc48`u-vBk$w@ zKk+bzpdl40>nH?~*Zfn;%>9#r@UtbfQn+W`IvhT9DAUY;p_F<%5osdACqME5e?4Ei z0?8VO*3fQ<$8b#HP)gkfqu80e(I+4rMCC zmp}K($Mn8FrV)VFc4L#g0Km&Dl?oP(EwXd{cwUHa?pjXWb>f{|P;52(#r27rog28Y zFQ5N8Phl1e-%aSa1I2b>Du{$S%CHxLCYZ{pbslbNth9KATY#YmtVpSOO|MNUgbi6U zmDva)MXS|Dr`^G9vxzKAMXS}W%r<8$?N)0fO|vzI&}0^>Qd$Aj1~5aQNoGFS+i2`n zTJIvF-7p~KJ^%-aXi5+r=_F~pS}E%^HR6>F#uK0N3{yt4-!siJ968cN7zSAPlkY#K z3-FgUDFu==MVe(d2U5N^3d1F^B{p&nnW!)xZr*qY1XB9m5UsTz1t;ubA#8qLDwS~J z2`AvbPA8a}n*Kuo8<_di%#15ubR9lx!}Mog`gDBz`#%B!XwJ31ABEvtWElK&Z(nb@ z-EO1Z>6j&4K%v<;GBNu4`q0jH0S1Q1fjrYGh_f-%sivDo=DT|APL$dNbF|TyR_!r0PjnZ z^dJ#U3qiAS7_}#7XE};OpAKbNhC9Fit;c*Z{!jL=|NfuoBnb{3o(PsKUiAJz2G=WP zqN~UZYz1t|&(4qZOb<8K&8>qh%MgaaJcXx+Yy9oszaRJ9a}V0>&PF1-mWY1hG$lX& z;T#XR^|m`KkSI%Omj12Q`n3}#yoXlG_YD+h4;oy7Ff}!e4I4I~lXQM6gm?~Xy{Fyo z;A>z0>|=IcACm~++LykLz-wN2mvs9dU^|*2O1!t-EN~b*Fw9~L95k5k|b!g zT0yJTs@p${o6R``K!L%E0C1|5o+CjsM0A)z2bxJ|uMlDfGvBF!TT4+iS1JGMO+0?> zt8c)2{`2GLY1A{V`LiHg6;MzX1q{do#RO=jynv_&2N_7x1Z15Y?15pOCxn!q8Q0g> zk5#Ky;lYOxYKMQNI1a&q#& zNzR#yh2f$!dKwrU9K@bId(KiyT~n{s|9-CZ*vg1MCPbjq&pZc0NC-izl~Vs)F2|#* z#>Y`9mwi*mJZUGIFW%`w{-VgIAXfvH^H_elU?@CBV18g9f@}UMgl59J0{`EXHwck+ zGH#KiX$r8m_1)qu0qLU=SAqvb8&aa=0J zJyHgvL^RIKXR_8$1>^ZbP@j~+ZLD>A-jgrHefRwIF`AAznwM)9L)st+ zP%4>`X1zVV80hcE;NTEOM@KQXXcUVVFUIKTD29he(AV3GOsP-^d1j+tzbA~M2c!(J z{@$NGCX4WwwQqg>t9bTvuRst6Evoeo9o1 z6FA0T5Co{zYM7pyMyJz04ZxAPR`Z8(T*jGapNo6%G56=gAN&tobKUjmbP_~Sv^!1H z2_m{U48v&7rfKZvQrg2OvyoX41W3~qGcz+oL^N3|m%p223MZa?8a6(-@Z-QAZpXA2 z;6*RK0a0YSqoOc;I)In-^!8wQco<44FV>M6AxPxUk(|>o^ZW`;K}u;4<~g;OVy)G+ zV!9y?hWX1rEnvH`+-ae;k$HBtTO;#i{@Y1TX>IE8fs|m)2%`w>fj|@TXUe=+nx+sa z(iTw|H|+#XfIvvk6og%DQ55;b>q7k~47{$POJ{_wWlJd$xTvBq6h$dBv(CN74Y6pvo*zQv0dBPx|VPn+#V&aw20@)Is?ool}fnc z>RjaWIhS4J-Y?af->J3!YPnKDZ*OnzgsqmPbMjg<;yA|e&@jMQ#jIa01c({9{CQU$ z^ZR+sQh=Af{I_j=UVwn#2Kcv!hKIzGB})L0w&pFAp6nxXmY(4Qz+(W#HeO-esYT~q z*l*_m+7v-q(FNZkOpfql9)`gaEDKP~7ZWkqf)}N*r@S&@qIC$a>^E=u5y7#?3GG@ni|qtuT)BZLn+-PBAjs2 zsd#9;!33_q;S~k5j8jx8%P&!vB3W5Lx z_TZl@DFuN$+B!-bwLJ3nb`2v;VG-^F_C=ov2hsMpL#B8kU;>z zsMVB8C6vo$l*?uFTufR9GWBonbUJ&bl>bl)!|%!uXa`6secI=ng8&2w40tHXH5 zI_>s+Iy;#PR=HBf=;$c6ZQHgO%&%m^Yr&LV^6V?|wJ&|hGT(| zAm5~cf_jXyxt|*yChe)sFtzziSV!YDG1-?BT6 zV&|(xtw%W-tH~MTHhWfs5#*K$0XL>@&}R{GpRs=422A z9t#SjSqq6R-zJ?7?!NmTOioU=l~RB7x&Qj;yPk8!)rg`9pZUax{rC9NEw_T1QLR?w z^z_V|nek_Hb4{^t|NdMEg!A;$;YSZW^bn?|rwG>FKGTaKZ@)0z)?O3z%~C7Fa%jo4TTvIoVv;=1y+c>`^>D z9e+S8J%2W!Z4y$2klhh}O#TOGWzq)s8W&C&aL?xFN*nbTZ8o5k(u14QGthLNk6S0^ zPbDW&tu>;^%oi{q5BbnlDP5dn5Cnlzsu~7CKM^e!QY>f9Co=QJQi>;xj*f~m&pgw2 zRSNrz-F*&ZU?LUL&cO%PKY$}g zj_g-Ty(~?$TWXCerl)3b%a?Kz&rM(bmeB@Ks!vM!egH3+n4HAK;fXxW)fzz{y}rf4 z0|&5q%VvP_(N??t3PIFSTI1VayXlxbhXqdolu}r`W;FneF!LWqQB)rr8$%d`=A^a6 z9&JN0va(%y@#W_X@w8#FSM%mv=#yP(B^L?y$htEkK&jN17~Q`+9RX&t10NVFEs${} z`>C{Sk>4UK5x&%Dfi>kK4dDSwc(77?aE<_I2tVnj$ObG{1i~;xR4SoXt)fz?*kPpt zQ50EubEE)f+wIPwEX#HiXe$Vtp?NbAZv$u#5hjI@b6RWV>(6Ee9^yDgb8Zfq(um73 zo_4{x3%zN_q7GmXz+xg=K}6%M^|&Bfu33)}s1F)d z05U2?7#$hKiEByDRU(Vke4}Zxm9>L`tb0@21W05h$&ZW(q=@NYhk$3~4Px zW;G%-!XWelpacB_ST(*1>+ipRBva~d!YJ6$ZgtiRA#lmFUVyKC`E&LfJiBmGGyl0D zI-#f0I4w&v%*;&tR2PMkbzgrU8V4Jgnx1;Ll=3NH{)QCCBv3bNb0OdC1uwb|QVKlj ztn+cly?4G5jJJ-Cj?#)1%jXYlu&2Ib?nws-7Dj%%8=@_eZ6X8h8-u*Omuvm?4M4dj zfE0y>A1=Jcjd}LULY$`?_KrBP&E=Ot80NsFAj<=AEWX^4xl&VL$tF24l~*rHamoKq zAOn=kG2(I=Fm>*CreS( zOtX|>!7LP_EhkcuW*MG%_NfcHh@SA|X8<;$GaF80wmIjAvTCidZ{I$QjEs~tbAx~( z&Afz}R}#@`0IOK*1Xj@|3vSkY%+OPpx zmfh6pBrg|&4g;7M1$^Q&UqYIss5NR}t)G)+*$1*L8#r*_AZDAhcFY*tz83Rc4jw#& zjhi+>W$EVudI^9zfN|^B7CseNPzP}Nb6<#ZIffA893jN#Yt`D)lTJFx)h>5$2%J-1X9ZZIC=H51zjwU zyWr{mB$xJNwG(u_qbtqLA*od3+2(8?5e-WrmuRiWHS;)Yy^6t0!59Y63!<{o>wiwl}ZKmdL8w84Lv=*KJtm8&{kB)5C1gfTIn5)Ybpc&`@DeS@J<3Dw4uFLHcD_r zAgv3MW5xlvG{KRrl>MC~O|fp>I_%lAm(w);2d(uR0VGmNeEIWvOZF!}_hq+ENh|dy zN~yOro6TU~{(VT2WZuYo*eS^mJ+uKc(^E|%dWqKjgg>(SdTz4ay zb93mlJLR6<#(#uCc+IL+tFUO%m=#G>Ua~3A|4qNX6!TSjl?S;B;l0hF-R7SM?iDsP zVSWczF>UaTK!tDah#>4b#E?^p$%=)zI)r5NuY=y8Blpb0C`2jBD-zXO4V6m8O#BHgu40|56cSM>f)s^nwdXkqzv&qU=@%B{b!g{=!$#Zg;?}>uH+3M+kAvkt0WN@ZiB*w%oE&!NB2(!`QNA z3$ir(dX{BZ5z#ae@SShmyr9o|K`?-q-S`?DICKcTJq=VVmFF?@^Ll%GF*Gz}NiGcS za%syx^6EFt(9k&r*se2^Z&@HuYp`HX-*nPW#Ca4z1xMld_|BZeF*V`b5wQh){DdJp z>d6svJO;t0hgjH!8AcH*l?ob-I_mX0q9`(r%>4TDo(sQAas0BQD2?OED_8 zLJ^fp2<-iFIu5XG(0q?GpkY{?%$5Qz?9^a~-)NAK9>;;}x3K&W02b6V&oc}{XlP#! zc99%hiP=^G9HUUVNP=>;DApPpxf~r4p8Vwvgc<8A?AfE-$$L2b% z1%3Wls0iStH~hBSa7&r-xiAdRU9)D5XFxg74+YfTg=(&~mmYSKyN*5A6Hv_6os)1s z69poGBLKNhuq?|g!Y@2P=r(t8(vWdDR>j3Cx5TBAsh`Jj;iQTor9>Q?Q!EH16KGCp zJt>9QC#Br3wcetX+9ZV7Dg^CPN*ySd%SU7&6V`@^BdlN~Gb1jQJTG9dueRV1`AJW^ z6cA(nLP?HnJGQia5Q1_67`IMb)rDLI!0{lgBTwyy zHjx+kwqQZRSAksF&^aPk9yE+g)jW}wNMr!?z5ft1^&6q|`?lN55RQfjx9@?adtv$M_F)U^q_(PIz*(Qyb1YetZ~4QJSW81`lejmoQ_cR%)D?$4Q7~TB$K+ z?gdd*2qD~wUbgvpt!h%5QYl8QYVMV^W4v71(<$q?XFP98lLKn4Zl4f&BiH;*4h!T8 zkG?y{?aHRo;zv>nPrS+-unc=tYmLa3Rf8b#(MM*9 ztyagPMT_Q1VLMK`)_Hdx5g~96f^)$Q7XrBsF8?_;EKUoIIakLQLg#}aTz8m@I4mgj}@D%hxR_XAuB}XB_dyl@5h% zsB*(Sl-3~Y9N1E|Q;^O51K_kurEU^x-fFpSSvi)fgTgDU9&43P1Lanp!#e6WWU< z3V}3D@}>-iKf#I$J4PuNIX=J|f}}_5Nk{~NM72^uT#iwW%eF1H;t_W;iz8!TR;la| zfIVRlY-8q4%-Af5wgrLQ!;DGAJSzn$x9DBi90UQB%8;fRs?{=1Ibp?uZuF~P{4&(4 z6--XgSfG(?m!hjJ6L#+2OT$A0Wdijw^GK%DGG<TCJg6E}L+^93!-37l#43-RUsEyiGcvxC_4l^S%z(q30qt6cG4%Z$>na z%AHHI<-u?u17FW~-Iue245ieF@k|L}?>2mswo^SoWri z7F@L_O^upRnq{`vpKT{$hEB&&eb?Q4FD54@W`iL3y>_Slp<1nmEK~UASM%PA4}R=3 zme)bcGo?NO=JO^dConxVor~01%JP=2Td;rMehtDO3nBjA;TK=~(xX=nI_e1Em9Kd% zlwtr0!XUVcnJ*t49K_(@kUyc*OxyB{@KjsBNHoLH`3-2}`0rqb%51J$5V+KZUT)Kg zXTtVUYy>eXl`6_*bE3;YdM22WWLGmJ=%666M}xNtAvS@r8O+-O>@7vnq-NCwLqGf+ zlD0FM4-rW0*2;jsZB3SJ>3UDfhb6P-O}f+7v<< zJV3KX0P`$e3D2vU{TW+$>CB>;Ht-%8uHg3<9`qLngAM5GqS)3#Z=9+W1$HRQr zk&zKhPfv?>yYq6b^ydh))r07d(vCU;c+*?nfvsD&Kq)m&B)(9JqP3@-atazfJ^rRR zQ%czuNaTycP2?#Ebec#ZC+olhFkuuSj!TH+7;zkY1+2&kNt->yP$TEdM<{ftJW(Pqiao*!jU$9O7q8Go+N_tDf z2(%H7i-O!ADTP!@*96f(rm{s!^Kvk+1~4v&R%A+zDWwJg)QLo3T8vH38rtY4j$_nn zHAJP7`MEh|%mU6-K6jy*o^g>BRUl0j%c%uPt6X@K3hGHY@yh8b2qFDuRvvhT*B$iA z%C3%Yahjr#i>GAU=i{^w91h@k0I(eXBHrZq39g60oa6%KvXS6+49v(X0p)Gi@J_}3 zz9$D+6(1=j(j@g=n}#2u;3Uen@7RI$>(?Vmk{<%N7Qi-lKb;f!*r&gUG|ezHG>D1A z6R!dA5A9AT-nY--0S;f#T4TqKo!Gs57nt!El}hz5o6RH8TI1GR9vwyas3U;6xhC$s z^T)XG!lz!BB+1%EV~bF$RiUjgh68_G=Ti`b9(XYt01BG4N8ZCIL|K~CsuV@2)@mq4 zC4`Y#+^%thh}s0qF!KQ+yhbGzMTL5sho5 z#{n!?S}kJ60Bc>9QVJ=A+0aplC^VbgMLeZags^y$`+M&YepBltX#2XWH3>}@bECErlzoC z$By#=yj3anI{?iT)14Q;@VWTUk9`J*4^Kc4{U=arm%hZ<_ltG&qRE zhYq3DYQ1!BZuY;3!~+XK5kBg7c*|e^9f+u;wf+JDmktgM8os}MFiK?xq_SRJ%DgpI z27y1#<2d#wc^Cwqbr;B>#lW;?-Vf*dr1hGrg$qG$;LE7LR`mqLsS zA(oM#Q3CZDMw~DkJ&F*ON~lySD3|k%9@`E-$7gU6lhLzRUNfP{xO5x2NC$QiK+yr1 zk}T)tI=}T?&QD&e!R5T}l=PXpC$kg^ICfLPS8Q^Mx3Z;3XTFr{(gLsjn142GUEVi| z6_lXOoIqg(T2N3=%H_{p1i=85#ZxMkGKdJBPGU-WHq3W|FYX?C7GNQT(acCXiFYtb znz#v35Q5Ndw{hQn_v7HfgBgIo(ptTZAXKK5pUv?84}S`ATyiDtG1mMsrPPH})63sy+&dghaAlO8LwgR+MYjqI7472VOyMEXr zz`jujz&WR{S>l)*!^})s5VycBQ~f zDL+tEbFNV1%d&+KnrJ?~+Lihoxr+pOxpN1~IRFp^W}R1Oc21$7^z5}CkeB~*;FrC) zM==GUf|{?elYpJ0vf{JzQW!zPa`?5+{gs`xR58@`99yvWAt-1sPyu7e9LL171@$}y zVrAQi-TTeV%wXNRb(o!`r##-^%I%#Nv-wr!-o!Gw%Ig& z0sxXE!9yD!!t9aR-OT)K06$BT6t~^_mB-!zeD(`BqZGx+RC;Z>QvHu=r4lRa@m;&;RIvHb9X0Lp~=OS=e!l$uIKUTDL?r=ZbuJ`DBOuDY_p5DO2;`9&v|jD=;V4d zlyy)}t*J;3a-^c-(OjBH+PccTGo7LrBX(krnv3aTL1w6`LRz!YmIuf zin(_CWdwRxC+SoV>_1>8Ab}x;#NK^-v2ELSFzbKUO21ZXomH!qdA$dZi39lfr#^3o zdQe=hRQ{k+sr*i*QW*wO0`LTYJw&u!E4?|(vQ5mqO>4aiz+oZ8Y#4@Vv(-YHBz|g+ zlS_KjKffDq`rTLI$xl3MK{xt2PrT5C?TwmI_VvB_R=`6_Fh~lZ!h`{U770Pil+xn> zk4qV>1bDF!Vo(s(g`kkEnoAtVs8*_|RI8}hYp7Lg2G6ivWK0U{p&6dcBL{GsustD6_I$@g;|`9W?H6Q`sN0&0^z}#ZDrpw8Tm7QYix2p=~WFJfDaYCBN=PkG}UJ8LE%@E~eE&B{}QB9R8 zM5PE(7|mSuB8~tc|d%u$; z!OopKv0=kTq-pY9A>~V0>s_v-{e$m*0|4-WkAB)#okXORemetin4O))p+kp!RFNhr zHg4L8sj2CM%=-C6;`WzZcO71K%?ln|2LJ%yxb^!8f&kS@-|Fwfhd=RImv#*^_`?7$nVOu! z;fcfE3GUms58HR_K)D?M>se=>`MUKFJiwp$(EE;RzdR}?V56tU2r49rj-u!R01xEj zvj7nY+ev0_%89F%EXXDYfb*VuF;$FEx@STCMmnJ_^lZa;JvToXbT^iX|mSWA$n` zj`uDESO~P+9gimnQQ)W}XH3TBGE|nCT+|PkfIks6b72sr^7*^VW3x3^TM*Bc1x?$v^kdVC6PVeOTGA}_uW?7uynLSsG;c_Y1r@cO z2JNRg+a@wAz#=^{NjPsZk4w<$nBg3iN+rMd_B{~6gT+od+;tyX`El>)q!a)J5cazX zTfuM+(4{aAh;`AP(i)NoD^{$)%*+f9PfT0~!iG+g{B0lu{lup{19yDyn|^KEb#MT{ z8v(3pG#V!)X@c1!vtZWf>FvQvxdP0*X3ySzrM|wtqaG*pm`(>i8oS`aOEA}Nqpw~^ zW@qgsiK)ptV6IlHVQP9hmQwTtQVxN!gjuiB%&UbEtF+QfA!r0ZA2XMQ5EKTc8d0{q zf=abw+Ec4F6VjJTkTS3;GiH@Pd=|aSN1f8ITg1W*^ROG=k1X@L{mN_lw>lj|HgWZO zt?OjG{=Z1qTg_P2|L(LThcYs!+rRoEelqj||*IU}05!+;{V(Nm~sv z)0HBWs}*Evh9I;gCC$D(TdV}g0zacjV3JazWk+|pHsmz5qu*@XxKb*Aem0HBovgsb z#3b&$?_Nw#O;2d%SAhBR(yp-|+Em~}AO9?bAS7u9DaF-9^qw?L8wU>@M7z^LZ*MQa zjM?Vw``4|z@3+UtSL%1X<NSC#h>I3b@z&~GVerl4JxiIRFwNd zL3_?90a=-FC-Y<%rz_nV)>!5@n$!2|SPaCKK#5HGh(xZFAcR1Yn3)NVspd&jg<&Ty zA62%q&W###;k*$TCC|NZe0*1FAbV}ji8y+!=SETv_@NL=JI5tuVd9c6DHcY%S$eIn z-8+eQ*W6UUZUVo=$~(jcaM%yLT_vty_mANgpD@HA0A=xk0f%_|Dh8 z6JTbvJDs3bt-L8JmEITxp>%6=wmG|fYHIr0QWX8*(Bu^UwVlcb8nN8KC@0x�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 Date: Tue, 5 May 2026 15:04:52 +0200 Subject: [PATCH 055/169] =?UTF-8?q?chore(release):=20bump=20to=20v1.1.0=20?= =?UTF-8?q?=E2=80=94=20theme=20foundation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HellionChat/HellionChat.csproj | 2 +- HellionChat/HellionChat.yaml | 63 ++++++++++++++++++++++++++++++++++ PRIVACY.md | 2 +- docs/CHANGELOG.md | 52 ++++++++++++++++++++++++++++ docs/ROADMAP.md | 37 ++++++++++++++------ docs/THIRD_PARTY_NOTICES.md | 4 +-- repo.json | 14 ++++---- 7 files changed, 153 insertions(+), 21 deletions(-) diff --git a/HellionChat/HellionChat.csproj b/HellionChat/HellionChat.csproj index cfe8103..1771783 100644 --- a/HellionChat/HellionChat.csproj +++ b/HellionChat/HellionChat.csproj @@ -4,7 +4,7 @@ 0.1.0 is our bootstrap release; the underlying Chat 2 base is called out in the yaml changelog so users can see what it derives from. --> - 1.0.3 + 1.1.0 enable enable + + Tab-Icon + + + FontAwesome-Glyph für die Sidebar. Default greift auf Tab-Name oder Channel-Typ. + + + (Default-Mapping) + Klassik (Chat 2 Default) diff --git a/HellionChat/Resources/HellionStrings.resx b/HellionChat/Resources/HellionStrings.resx index edba1d7..67a1f16 100644 --- a/HellionChat/Resources/HellionStrings.resx +++ b/HellionChat/Resources/HellionStrings.resx @@ -561,6 +561,17 @@ If you use multiple linkshells, the maintainer recommends one tab per shell for cleaner readability. Duplicate this tab and narrow the channel selection per copy. + + + + Tab-Icon + + + FontAwesome-Glyph für die Sidebar. Default greift auf Tab-Name oder Channel-Typ. + + + (Default-Mapping) + Klassik (Chat 2 Default) diff --git a/HellionChat/Ui/SettingsTabs/Tabs.cs b/HellionChat/Ui/SettingsTabs/Tabs.cs index 8ad2bb0..75eea88 100755 --- a/HellionChat/Ui/SettingsTabs/Tabs.cs +++ b/HellionChat/Ui/SettingsTabs/Tabs.cs @@ -91,6 +91,40 @@ internal sealed class Tabs : ISettingsTab } ImGui.InputText(Language.Options_Tabs_Name, ref tab.Name, 512, ImGuiInputTextFlags.EnterReturnsTrue); + + // v1.2.0 — Per-Tab Icon-Override. Default-Mapping greift falls nichts gesetzt. + ImGui.TextUnformatted(HellionStrings.Tabs_Icon_Label); + ImGui.SameLine(); + ImGuiUtil.HelpMarker(HellionStrings.Tabs_Icon_HelpMarker); + + var iconCurrent = string.IsNullOrEmpty(tab.Icon) ? "" : tab.Icon; + var iconPreview = iconCurrent.Length == 0 + ? HellionStrings.Tabs_Icon_DefaultOption + : iconCurrent; + using (var combo = ImRaii.Combo($"##icon-{i}", iconPreview)) + { + if (combo.Success) + { + // Erste Option: Default (löscht Icon, lässt Mapping greifen). + if (ImGui.Selectable(HellionStrings.Tabs_Icon_DefaultOption, iconCurrent.Length == 0)) + { + tab.Icon = null; + } + + ImGui.Separator(); + + // Pool-Optionen aus TabIconGlyphResolver.PickerOptions (Single-Source-of-Truth). + foreach (var option in TabIconGlyphResolver.PickerOptions) + { + var isSelected = string.Equals(iconCurrent, option, StringComparison.OrdinalIgnoreCase); + if (ImGui.Selectable(option, isSelected)) + { + tab.Icon = option; + } + } + } + } + ImGui.Checkbox(Language.Options_Tabs_ShowTimestamps, ref tab.DisplayTimestamp); ImGui.Checkbox(Language.Options_Tabs_PopOut, ref tab.PopOut); if (tab.PopOut) -- 2.52.0 From 985a284e7ddce286538c3c8dde48bf01e86ee289 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 19:34:27 +0200 Subject: [PATCH 070/169] feat(statusbar): cached 1Hz status-bar component with format helpers --- HellionChat/Configuration.cs | 14 ++ .../Resources/HellionStrings.Designer.cs | 4 + HellionChat/Resources/HellionStrings.de.resx | 6 + HellionChat/Resources/HellionStrings.resx | 6 + HellionChat/Ui/StatusBar.cs | 171 ++++++++++++++++++ 5 files changed, 201 insertions(+) create mode 100644 HellionChat/Ui/StatusBar.cs diff --git a/HellionChat/Configuration.cs b/HellionChat/Configuration.cs index 5fdb909..557e8cb 100755 --- a/HellionChat/Configuration.cs +++ b/HellionChat/Configuration.cs @@ -603,6 +603,20 @@ public class Tab } } + /// + /// Aktuelle Anzahl der gespeicherten Messages. Lock-acquire pro Read + /// ist OK für 1×/sec Status-Bar-Polling (v1.2.0). + /// + public int Count + { + get + { + LockSlim.Wait(-1); + try { return Messages.Count; } + finally { LockSlim.Release(); } + } + } + /// /// Returns an array copy of the message list for usage outside of main thread /// diff --git a/HellionChat/Resources/HellionStrings.Designer.cs b/HellionChat/Resources/HellionStrings.Designer.cs index 784a394..bec7fff 100644 --- a/HellionChat/Resources/HellionStrings.Designer.cs +++ b/HellionChat/Resources/HellionStrings.Designer.cs @@ -315,4 +315,8 @@ internal class HellionStrings internal static string ChatTwoConflictTitle => Get(nameof(ChatTwoConflictTitle)); internal static string ChatTwoConflictBody => Get(nameof(ChatTwoConflictBody)); internal static string ChatTwoConflictAction => Get(nameof(ChatTwoConflictAction)); + + // Hellion Chat — v1.2.0 Bottom-Status-Bar Privacy-Badge labels + internal static string StatusBar_Privacy_Enabled => Get(nameof(StatusBar_Privacy_Enabled)); + internal static string StatusBar_Privacy_Open => Get(nameof(StatusBar_Privacy_Open)); } diff --git a/HellionChat/Resources/HellionStrings.de.resx b/HellionChat/Resources/HellionStrings.de.resx index 6957f51..5adeb67 100644 --- a/HellionChat/Resources/HellionStrings.de.resx +++ b/HellionChat/Resources/HellionStrings.de.resx @@ -716,4 +716,10 @@ Behalten + + Privacy-First + + + Offen + diff --git a/HellionChat/Resources/HellionStrings.resx b/HellionChat/Resources/HellionStrings.resx index 67a1f16..86d829d 100644 --- a/HellionChat/Resources/HellionStrings.resx +++ b/HellionChat/Resources/HellionStrings.resx @@ -716,4 +716,10 @@ Keep current + + Privacy-First + + + Open + diff --git a/HellionChat/Ui/StatusBar.cs b/HellionChat/Ui/StatusBar.cs new file mode 100644 index 0000000..77a6811 --- /dev/null +++ b/HellionChat/Ui/StatusBar.cs @@ -0,0 +1,171 @@ +using System.Globalization; +using System.Numerics; +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; +using HellionChat.Code; +using HellionChat.Resources; +using HellionChat.Util; + +namespace HellionChat.Ui; + +/// +/// Bottom-Status-Bar (v1.2.0). Fix 22 px hoch, BorderTop als Trenner. +/// Slots links → rechts: Channel-Indicator (Color-Dot + Channel-Name), +/// Privacy-Badge (Lock-Icon + Privacy-Label), Counts (Tabs + Msgs), +/// Tells (Auto-Tell-Counter, hidden bei 0), Version (rechtsbündig, muted). +/// +/// Update-Frequenz: 1×/Sekunde. Format-Strings werden zwischen Updates +/// gecached, damit kein Per-Frame-Format-Allocation entsteht. +/// +internal sealed class StatusBar +{ + public const float Height = 22f; + private const long UpdateIntervalMs = 1000; + + // Cache-State — initial outdated, damit der erste Frame frisch berechnet. + private long _lastUpdateMs = -UpdateIntervalMs; + private string _cachedCountsText = string.Empty; + private string _cachedTellsText = string.Empty; + + /// + /// Reine String-Logik — testbar ohne ImGui-Init. + /// + public static string FormatCounts(int tabs, int messages) + { + // InvariantCulture: User-System-Locale darf das Format nicht + // verändern (de_DE würde sonst "1,2k" statt "1.2k" liefern). + var msgPart = messages >= 1000 + ? string.Format(CultureInfo.InvariantCulture, "{0:0.0}k msg", messages / 1000.0) + : $"{messages} msg"; + var tabsPart = $"{tabs} {(tabs == 1 ? "tab" : "tabs")}"; + return $"{tabsPart} · {msgPart}"; + } + + /// + /// Reine String-Logik — testbar ohne ImGui-Init. + /// 0 Tells → Leerstring (Slot wird ausgeblendet). + /// + public static string FormatTells(int count) + { + if (count <= 0) return string.Empty; + return $"{count} {(count == 1 ? "tell" : "tells")}"; + } + + /// + /// Test-Hook: Cache-Logic ohne reale Time-Source verifizieren. + /// Nicht für Production-Render. + /// + internal (string counts, string tells) SnapshotForTest(long now, int tabs, int messages, int tells) + { + UpdateCacheIfDue(now, tabs, messages, tells); + return (_cachedCountsText, _cachedTellsText); + } + + private void UpdateCacheIfDue(long now, int tabs, int messages, int tells) + { + if (now - _lastUpdateMs < UpdateIntervalMs) + return; + _cachedCountsText = FormatCounts(tabs, messages); + _cachedTellsText = FormatTells(tells); + _lastUpdateMs = now; + } + + /// + /// Render-Pfad. Aufrufer pusht bereits den HellionStyle/Theme; + /// wir lesen nur die aktiven Theme-Farben und zeichnen. + /// + public void Draw(Plugin plugin) + { + var theme = plugin.ThemeRegistry.Active; + var now = Environment.TickCount64; + + // Counts pro Frame berechnen ist günstig (List<>.Count, kleine + // Sums); Format-String wird gecached. + var tabs = Plugin.Config.Tabs.Count; + var messages = Plugin.Config.Tabs.Sum(t => t.Messages.Count); + var tells = Plugin.Config.Tabs.Count(t => t.IsTempTab); + UpdateCacheIfDue(now, tabs, messages, tells); + + // BorderTop als Trenner — DrawList-Line, ImGui-Separator hat zu viel Padding. + var cursorY = ImGui.GetCursorScreenPos().Y; + var winLeft = ImGui.GetWindowPos().X; + var winRight = winLeft + ImGui.GetWindowSize().X; + ImGui.GetWindowDrawList().AddLine( + new Vector2(winLeft, cursorY), + new Vector2(winRight, cursorY), + ColourUtil.RgbaToAbgr(theme.Colors.Border), + 1f); + + ImGui.Dummy(new Vector2(0, 2)); // BorderTop-Spacing + + using var group = ImRaii.Group(); + + // Slot 1: Active-Channel-Indicator + var inputCh = plugin.CurrentTab?.CurrentChannel?.Channel ?? InputChannel.Invalid; + var hasChannel = inputCh != InputChannel.Invalid; + var chatType = inputCh.ToChatType(); + var channelName = hasChannel ? chatType.Name() : "—"; + var channelColor = hasChannel + ? (plugin.Functions.Chat.GetChannelColor(chatType) ?? theme.Colors.TextMuted) + : theme.Colors.TextMuted; + DrawDot(channelColor); + ImGui.SameLine(); + ImGui.TextUnformatted(channelName); + + // Slot 2: Privacy-Badge — abgeleitet aus PrivacyFilterEnabled. + ImGui.SameLine(); + DrawSeparator(); + ImGui.SameLine(); + using (plugin.FontManager.FontAwesome.Push()) + { + ImGui.TextUnformatted(FontAwesomeIcon.Lock.ToIconString()); + } + ImGui.SameLine(); + var privacyLabel = Plugin.Config.PrivacyFilterEnabled + ? HellionStrings.StatusBar_Privacy_Enabled + : HellionStrings.StatusBar_Privacy_Open; + ImGui.TextUnformatted(privacyLabel); + + // Slot 3: Counts + ImGui.SameLine(); + DrawSeparator(); + ImGui.SameLine(); + ImGui.TextUnformatted(_cachedCountsText); + + // Slot 4: Tells (nur wenn > 0) + if (!string.IsNullOrEmpty(_cachedTellsText)) + { + ImGui.SameLine(); + DrawSeparator(); + ImGui.SameLine(); + ImGui.TextUnformatted(_cachedTellsText); + } + + // Slot 5: Version (rechtsbündig, muted) + var versionText = $"v{Plugin.Interface.Manifest.AssemblyVersion} · Hellion"; + var versionWidth = ImGui.CalcTextSize(versionText).X; + var rightCursor = ImGui.GetWindowSize().X - versionWidth - ImGui.GetStyle().WindowPadding.X; + ImGui.SameLine(rightCursor); + using (ImRaii.PushColor(ImGuiCol.Text, ColourUtil.RgbaToAbgr(theme.Colors.TextMuted))) + { + ImGui.TextUnformatted(versionText); + } + } + + private static void DrawDot(uint rgba) + { + var pos = ImGui.GetCursorScreenPos(); + const float radius = 4f; + ImGui.GetWindowDrawList().AddCircleFilled( + new Vector2(pos.X + radius, pos.Y + ImGui.GetTextLineHeight() / 2f), + radius, + ColourUtil.RgbaToAbgr(rgba)); + ImGui.Dummy(new Vector2(radius * 2 + 4, ImGui.GetTextLineHeight())); + } + + private static void DrawSeparator() + { + ImGui.TextDisabled("·"); + } +} -- 2.52.0 From a11c8bc6e971ec0bab2fe5fcae99eae604167783 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 19:35:52 +0200 Subject: [PATCH 071/169] feat(statusbar): wire status bar into ChatLogWindow render pipeline --- HellionChat/Plugin.cs | 3 +++ HellionChat/Ui/ChatLogWindow.cs | 17 ++++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index 6cb47f5..2b58150 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -64,6 +64,7 @@ public sealed class Plugin : IDalamudPlugin internal TypingIpc TypingIpc { get; } internal FontManager FontManager { get; } internal Themes.ThemeRegistry ThemeRegistry { get; private set; } = null!; + internal Ui.StatusBar StatusBar { get; private set; } = null!; internal int DeferredSaveFrames = -1; @@ -296,6 +297,8 @@ public sealed class Plugin : IDalamudPlugin ThemeRegistry = new Themes.ThemeRegistry(customThemesDir); ThemeRegistry.Switch(Config.Theme); + StatusBar = new Ui.StatusBar(); + MessageManager = new MessageManager(this); // Does it require UI? // Hellion Chat — Auto-Tell-Tabs service. Subscribes to the diff --git a/HellionChat/Ui/ChatLogWindow.cs b/HellionChat/Ui/ChatLogWindow.cs index 99f4083..1c826cf 100644 --- a/HellionChat/Ui/ChatLogWindow.cs +++ b/HellionChat/Ui/ChatLogWindow.cs @@ -375,6 +375,9 @@ public sealed class ChatLogWindow : Window // weil der Cursor schon weiter unten steht — kein eigener Abzug. height -= ImGui.GetFrameHeightWithSpacing(); + // v1.2.0 — Status-Bar am Window-Boden reserviert 22 px + 2 px Spacing. + height -= StatusBar.Height + 2; + return height; } @@ -790,13 +793,17 @@ public sealed class ChatLogWindow : Window if (ImGui.IsWindowHovered(ImGuiHoveredFlags.ChildWindows)) LastActivityTime = FrameTime; - if (!showNovice) - return; + if (showNovice) + { + ImGui.SameLine(); - ImGui.SameLine(); + if (ImGuiUtil.IconButton(FontAwesomeIcon.Leaf)) + GameFunctions.GameFunctions.ClickNoviceNetworkButton(); + } - if (ImGuiUtil.IconButton(FontAwesomeIcon.Leaf)) - GameFunctions.GameFunctions.ClickNoviceNetworkButton(); + // v1.2.0 — Bottom-Status-Bar. Letzter Render-Step in DrawChatLog, + // damit alle Zeilen-Operationen davor keine Layout-Sprünge auslösen. + Plugin.StatusBar.Draw(Plugin); } internal Dictionary GetValidChannels() -- 2.52.0 From b48684ce5aa2dc3f5214b91bf0cd93ba304b4b6a Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 19:42:57 +0200 Subject: [PATCH 072/169] feat(settings): compact density toggle in Appearance --- HellionChat/Resources/HellionStrings.Designer.cs | 4 ++++ HellionChat/Resources/HellionStrings.de.resx | 6 ++++++ HellionChat/Resources/HellionStrings.resx | 6 ++++++ HellionChat/Ui/SettingsTabs/Appearance.cs | 5 +++++ 4 files changed, 21 insertions(+) diff --git a/HellionChat/Resources/HellionStrings.Designer.cs b/HellionChat/Resources/HellionStrings.Designer.cs index bec7fff..bfbead7 100644 --- a/HellionChat/Resources/HellionStrings.Designer.cs +++ b/HellionChat/Resources/HellionStrings.Designer.cs @@ -319,4 +319,8 @@ internal class HellionStrings // Hellion Chat — v1.2.0 Bottom-Status-Bar Privacy-Badge labels internal static string StatusBar_Privacy_Enabled => Get(nameof(StatusBar_Privacy_Enabled)); internal static string StatusBar_Privacy_Open => Get(nameof(StatusBar_Privacy_Open)); + + // Hellion Chat — v1.2.0 Appearance / Compact-Density toggle + internal static string Appearance_UseCompactDensity_Name => Get(nameof(Appearance_UseCompactDensity_Name)); + internal static string Appearance_UseCompactDensity_Description => Get(nameof(Appearance_UseCompactDensity_Description)); } diff --git a/HellionChat/Resources/HellionStrings.de.resx b/HellionChat/Resources/HellionStrings.de.resx index 5adeb67..eea3036 100644 --- a/HellionChat/Resources/HellionStrings.de.resx +++ b/HellionChat/Resources/HellionStrings.de.resx @@ -722,4 +722,10 @@ Offen + + Kompakte Dichte + + + Schaltet das Message-Layout vom Card-Row-Default zurück auf einzeilige `[HH:mm] Sender: Text` Zeilen. + diff --git a/HellionChat/Resources/HellionStrings.resx b/HellionChat/Resources/HellionStrings.resx index 86d829d..7616d54 100644 --- a/HellionChat/Resources/HellionStrings.resx +++ b/HellionChat/Resources/HellionStrings.resx @@ -722,4 +722,10 @@ Open + + Compact Density + + + Schaltet das Message-Layout vom Card-Row-Default zurück auf einzeilige `[HH:mm] Sender: Text` Zeilen. + diff --git a/HellionChat/Ui/SettingsTabs/Appearance.cs b/HellionChat/Ui/SettingsTabs/Appearance.cs index ce09f4c..5cde2ee 100644 --- a/HellionChat/Ui/SettingsTabs/Appearance.cs +++ b/HellionChat/Ui/SettingsTabs/Appearance.cs @@ -356,6 +356,11 @@ internal sealed class Appearance : ISettingsTab ImGui.Checkbox(Language.Options_MoreCompactPretty_Name, ref Mutable.MoreCompactPretty); ImGuiUtil.HelpMarker(Language.Options_MoreCompactPretty_Description); + // v1.2.0 — Card-Rows als Default. Compact-Density schaltet auf den + // klassischen Single-Line-Mode `[HH:mm] Sender: Text` zurück. + ImGui.Checkbox(HellionStrings.Appearance_UseCompactDensity_Name, ref Mutable.UseCompactDensity); + ImGuiUtil.HelpMarker(HellionStrings.Appearance_UseCompactDensity_Description); + ImGui.Checkbox(Language.Options_HideSameTimestamps_Name, ref Mutable.HideSameTimestamps); ImGuiUtil.HelpMarker(Language.Options_HideSameTimestamps_Description); } -- 2.52.0 From d485f5ea1fad1f952cff6797d3f7308d3fed3e98 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 19:44:37 +0200 Subject: [PATCH 073/169] feat(messages): card-row default render with compact-density opt-out --- HellionChat/Ui/ChatLogWindow.cs | 64 ++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 9 deletions(-) diff --git a/HellionChat/Ui/ChatLogWindow.cs b/HellionChat/Ui/ChatLogWindow.cs index 1c826cf..9a83e00 100644 --- a/HellionChat/Ui/ChatLogWindow.cs +++ b/HellionChat/Ui/ChatLogWindow.cs @@ -1323,17 +1323,63 @@ public sealed class ChatLogWindow : Window ImGui.TableNextColumn(); var lineWidth = ImGui.GetContentRegionAvail().X; - if (message.Sender.Count > 0) - { - DrawChunks(message.Sender, true, handler, lineWidth); - ImGui.SameLine(); - } - // We need to draw something otherwise the item visibility check below won't work. - if (message.Content.Count == 0) - DrawChunks([new TextChunk(ChunkSource.Content, null, " ")], true, handler, lineWidth); + // v1.2.0 — Card-Rows als Default, Compact-Density als Opt-Out. + // Card-Mode: Sender-Header in Channel-Color auf eigener Zeile, + // dann Body, dann subtile Border-Bottom als Card-Trenner. + // Compact-Mode: bisheriges Verhalten — Sender + Space + Content + // auf einer Zeile via SameLine. + var useCard = !Plugin.Config.UseCompactDensity; + if (useCard) + { + if (message.Sender.Count > 0) + { + var theme = Plugin.ThemeRegistry.Active; + var senderColor = Plugin.Functions.Chat.GetChannelColor(message.Code.Type) + ?? theme.Colors.TextPrimary; + using (ImRaii.PushColor(ImGuiCol.Text, ColourUtil.RgbaToAbgr(senderColor))) + { + DrawChunks(message.Sender, true, handler, lineWidth); + } + // KEIN SameLine — Body landet auf eigener Zeile. + } + + // We need to draw something otherwise the item visibility check below won't work. + if (message.Content.Count == 0) + DrawChunks([new TextChunk(ChunkSource.Content, null, " ")], true, handler, lineWidth); + else + DrawChunks(message.Content, true, handler, lineWidth); + + // Subtile Border-Bottom als Card-Trenner. Border-Farbe mit + // reduzierter Alpha (RGBA → 0x33) für dezente Trennung. + { + var theme = Plugin.ThemeRegistry.Active; + var rowEndY = ImGui.GetCursorScreenPos().Y; + var winLeft = ImGui.GetWindowPos().X; + var winRight = winLeft + ImGui.GetWindowSize().X; + var borderRgba = (theme.Colors.Border & 0xFFFFFF00u) | 0x33u; + ImGui.GetWindowDrawList().AddLine( + new Vector2(winLeft + 4, rowEndY - 1), + new Vector2(winRight - 4, rowEndY - 1), + ColourUtil.RgbaToAbgr(borderRgba), + 1f); + ImGui.Dummy(new Vector2(0, 2)); + } + } else - DrawChunks(message.Content, true, handler, lineWidth); + { + if (message.Sender.Count > 0) + { + DrawChunks(message.Sender, true, handler, lineWidth); + ImGui.SameLine(); + } + + // We need to draw something otherwise the item visibility check below won't work. + if (message.Content.Count == 0) + DrawChunks([new TextChunk(ChunkSource.Content, null, " ")], true, handler, lineWidth); + else + DrawChunks(message.Content, true, handler, lineWidth); + } message.IsVisible[tab.Identifier] = ImGui.IsItemVisible(); } -- 2.52.0 From e404a2e0d937070d1a4452b10242e6800ea464a8 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 5 May 2026 19:47:09 +0200 Subject: [PATCH 074/169] Refactor privacy notice for clarity and consistency --- PRIVACY.md | 214 ++++++++++++++--------------------------------------- 1 file changed, 56 insertions(+), 158 deletions(-) diff --git a/PRIVACY.md b/PRIVACY.md index e52efd4..547a5a0 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -1,16 +1,8 @@ # Privacy notice -HellionChat is a Dalamud plugin for FINAL FANTASY XIV that focuses on -giving the user explicit control over what their chat client stores -locally. This document describes what the plugin does with your data, -what it does not do, and how you exercise the rights the GDPR gives -you over data you generate yourself. +HellionChat is a Dalamud plugin for FINAL FANTASY XIV, focused on giving the user explicit control over what their chat client stores locally. This document describes what the plugin does with your data, what it does not do, and how you exercise the rights the GDPR gives you over data you generate yourself. -This document is informational. The maintainer of HellionChat is -**not** a controller or processor of your data in the GDPR sense, -because no data ever leaves your machine on the maintainer's -infrastructure. Independently of that, the plugin is built so that -you can act on your own data the way the GDPR expects. +This document is informational. The maintainer of HellionChat is **not** a controller or processor of your data in the GDPR sense, because no data ever leaves your machine on the maintainer's infrastructure. Independently of that, the plugin is built so that you can act on your own data the way the GDPR expects. Last reviewed: 2026-05-05 (HellionChat v1.1.0). @@ -18,195 +10,109 @@ Last reviewed: 2026-05-05 (HellionChat v1.1.0). ## TL;DR -- All chat data the plugin stores stays on your machine, in your - Dalamud `pluginConfigs/HellionChat/` directory. -- The plugin does not phone home. There is no telemetry, no analytics, - no crash reporter, no usage counter, no remote update check beyond - what Dalamud itself does. -- One outbound network call exists by design: the BetterTTV emote - service (for chat emotes). It is documented in detail below and - can be reasoned about per request. -- You can export every message the plugin has stored, in Markdown, - JSON or CSV, and you can wipe stored history per channel, per date - range, or globally. +- All chat data the plugin stores stays on your machine, in your Dalamud `pluginConfigs/HellionChat/` directory. +- The plugin does not phone home. No telemetry, no analytics, no crash reporter, no usage counter, no remote update check beyond what Dalamud itself does. +- One outbound network call exists by design: the BetterTTV emote service (for chat emotes). It is documented in detail below and can be reasoned about per request. +- You can export every message the plugin has stored, in Markdown, JSON or CSV, and you can wipe stored history per channel, per date range, or globally. --- ## What the plugin stores locally -HellionChat keeps three kinds of state on your machine, all under -`%appdata%\XIVLauncher\pluginConfigs\HellionChat\` on Windows -(`~/.xlcore/pluginConfigs/HellionChat/` on Linux/macOS via XIVLauncher -Core): +HellionChat keeps three kinds of state on your machine, all under `%appdata%\XIVLauncher\pluginConfigs\HellionChat\` on Windows (`~/.xlcore/pluginConfigs/HellionChat/` on Linux/macOS via XIVLauncher Core): -1. **Configuration** (`HellionChat.json`) - Plugin settings, channel whitelist, retention values, layout state, - theme colours. Contains no chat content. +1. **Configuration** (`HellionChat.json`). + Plugin settings, channel whitelist, retention values, layout state, theme colours. Contains no chat content. -2. **Message database** (SQLite file in the same directory) - Chat messages from the channels on your whitelist, stored as - MessagePack-encoded blobs. Default whitelist out of the box covers - only your own conversations: tells, party, free company, linkshells, - cross-world linkshells, alliance, ExtraChat. Public chat, NPC - dialogue, system messages and battle logs are dropped on the - storage layer and never written to disk. +2. **Message database** (SQLite file in the same directory). + Chat messages from the channels on your whitelist, stored as MessagePack-encoded blobs. The default whitelist out of the box covers only your own conversations: tells, party, free company, linkshells, cross-world linkshells, alliance, ExtraChat. Public chat, NPC dialogue, system messages and battle logs are dropped on the storage layer and never written to disk. -3. **Cached emote images** (`EmoteCacheV1/` directory) - Image files downloaded from BetterTTV when an emote appears in a - message you receive. See "Outbound network calls" below. +3. **Cached emote images** (`EmoteCacheV1/` directory). + Image files downloaded from BetterTTV when an emote appears in a message you receive. See "Outbound network calls" below. -There is no shared state with the upstream Chat 2 plugin. -`pluginConfigs/HellionChat/` is independent from `pluginConfigs/ChatTwo/`. +There is no shared state with the upstream Chat 2 plugin. `pluginConfigs/HellionChat/` is independent from `pluginConfigs/ChatTwo/`. ### Retention defaults - Tells: 365 days -- Your-conversation channels (party, FC, linkshells, cross-world LS, - alliance, ExtraChat): 90 days +- Your-conversation channels (party, FC, linkshells, cross-world LS, alliance, ExtraChat): 90 days - Global default for anything else: 30 days -**Retention is off by default.** The plugin does not delete anything -on its own until you explicitly turn the retention sweep on in the -settings. Until then, stored messages stay until you clear them. +**Retention is off by default.** The plugin does not delete anything on its own until you explicitly turn the retention sweep on in the settings. Until then, stored messages stay until you clear them. --- ## What the plugin does not store -- Public chat (`/say`, `/yell`, `/shout`), NPC dialogue, system - messages and battle logs. These are filtered before they reach the - storage layer. -- Anything from channels you remove from the whitelist. The privacy - filter runs on the way in, not on the way out. -- Login credentials, character IDs, account IDs. The plugin uses - whatever Dalamud already exposes about the local character to - attribute messages; nothing of that is sent anywhere or persisted - beyond the message itself. +- Public chat (`/say`, `/yell`, `/shout`), NPC dialogue, system messages and battle logs. These are filtered before they reach the storage layer. +- Anything from channels you remove from the whitelist. The privacy filter runs on the way in, not on the way out. +- Login credentials, character IDs, account IDs. The plugin uses whatever Dalamud already exposes about the local character to attribute messages. Nothing of that is sent anywhere or persisted beyond the message itself. --- ## Outbound network calls -HellionChat makes two kinds of automatic outbound network requests. -Both are inherited from upstream Chat 2 and both are documented here -because "DSGVO-by-design" means you should know what your client does -on your behalf. +HellionChat makes two kinds of automatic outbound network requests. Both are inherited from upstream Chat 2 and both are documented here because "GDPR-by-design" means you should know what your client does on your behalf. ### 1. BetterTTV emote service (`api.betterttv.net`, `cdn.betterttv.net`) -- **What it does:** When a chat message arrives that references a - BetterTTV emote, the plugin asks the BetterTTV API for the emote - metadata and downloads the image from the BetterTTV CDN to display - it inline. -- **What is sent:** A standard HTTPS GET request. Your IP address - reaches BetterTTV (unavoidable for any HTTPS request); the request - itself contains no identifying user data, no character name, no - message text. Only the emote ID being looked up is in the URL path. +- **What it does:** When a chat message arrives that references a BetterTTV emote, the plugin asks the BetterTTV API for the emote metadata and downloads the image from the BetterTTV CDN to display it inline. +- **What is sent:** A standard HTTPS GET request. Your IP address reaches BetterTTV (unavoidable for any HTTPS request); the request itself contains no identifying user data, no character name, no message text. Only the emote ID being looked up is in the URL path. - **When it triggers:** - - The emote *list* (global emotes plus the top-1500 community emotes - over fifteen API pages) is fetched from `api.betterttv.net` once - per session at plugin startup, provided the **Show emotes** option - is on. This first list-fetch happens before any chat message has - arrived; BetterTTV's edge therefore sees your IP as soon as the - plugin loads, not only after an emote is mentioned. - - The individual emote *images* on `cdn.betterttv.net` are fetched - on demand, only when an incoming chat message contains a token - matching one of the cached IDs. These are cached locally - (`emoteCache/`) and reused across sessions. -- **Cached:** Yes, in `emoteCache/`. A given emote is downloaded once - per machine and reused. -- **How to opt out:** Turn off the **Show emotes** option in - Settings → Chat. With it disabled, the emote cache does not load - and no requests to BetterTTV are made for the rest of the session. + - The emote *list* (global emotes plus the top-1500 community emotes over fifteen API pages) is fetched from `api.betterttv.net` once per session at plugin startup, provided the **Show emotes** option is on. This first list-fetch happens before any chat message has arrived. BetterTTV's edge therefore sees your IP as soon as the plugin loads, not only after an emote is mentioned. + - The individual emote *images* on `cdn.betterttv.net` are fetched on demand, only when an incoming chat message contains a token matching one of the cached IDs. These are cached locally (`emoteCache/`) and reused across sessions. +- **Cached:** Yes, in `emoteCache/`. A given emote is downloaded once per machine and reused. +- **How to opt out:** Turn off the **Show emotes** option in Settings → Chat. With it disabled, the emote cache does not load and no requests to BetterTTV are made for the rest of the session. - **BetterTTV's privacy policy:** Source: `HellionChat/EmoteCache.cs`. -### 2. Square Enix Lodestone font — removed in v1.0.4 +### 2. Square Enix Lodestone font (removed in v1.0.4) -Earlier versions of HellionChat (and upstream Chat 2) downloaded -`FFXIV_Lodestone_SSF.ttf` from `img.finalfantasyxiv.com` once during -font setup. That code path was a leftover from upstream's removed -webinterface feature and was no longer consumed anywhere — the in-game -symbol glyphs (job icons, item glyphs, status effects) come from -Dalamud's bundled symbol-font helper, not from the downloaded TTF. +Earlier versions of HellionChat (and upstream Chat 2) downloaded `FFXIV_Lodestone_SSF.ttf` from `img.finalfantasyxiv.com` once during font setup. That code path was a leftover from upstream's removed webinterface feature and was no longer consumed anywhere. The in-game symbol glyphs (job icons, item glyphs, status effects) come from Dalamud's bundled symbol-font helper, not from the downloaded TTF. -The download was removed in v1.0.4. As of that version HellionChat -makes no automatic network call to Square Enix or to any -`finalfantasyxiv.com` host. +The download was removed in v1.0.4. As of that version HellionChat makes no automatic network call to Square Enix or to any `finalfantasyxiv.com` host. -Cached `FFXIV_Lodestone_SSF.ttf` files left over from earlier versions -remain in `pluginConfigs/HellionChat/` until manually deleted; they -are no longer read. +Cached `FFXIV_Lodestone_SSF.ttf` files left over from earlier versions remain in `pluginConfigs/HellionChat/` until manually deleted. They are no longer read. ### Links you click yourself (no automatic traffic) -The settings panel contains a few buttons that open external pages in -your browser when you click them: the upstream Chat 2 GitHub repo, -the upstream maintainers' Ko-fi pages, the HellionChat issue tracker -and `hellion-media.de`. Nothing happens until you click. They are -documented here for completeness, not because they generate background -traffic. +The settings panel contains a few buttons that open external pages in your browser when you click them: the upstream Chat 2 GitHub repo, the upstream maintainers' Ko-fi pages, the HellionChat issue tracker and `hellion-media.de`. Nothing happens until you click. They are documented here for completeness, not because they generate background traffic. --- ## What the plugin does not do -- **No telemetry.** Source verified: no calls to AppInsights, Sentry, - PostHog, Plausible, Google Analytics, Microsoft Clarity or any - comparable service exist in the codebase, nor in the direct - dependencies the plugin pulls in. See `docs/THIRD_PARTY_NOTICES.md`. -- **No crash reporting.** Crashes go to Dalamud's local `xllog`, - not to a remote endpoint controlled by HellionChat. -- **No usage counters.** The plugin does not count installs, sessions, - feature usage, channel activity or anything else for the maintainer. -- **No phone-home update check.** Updates are delivered through - Dalamud's plugin installer, which polls the custom-repo - `repo.json` on GitHub. That is GitHub's traffic and falls under - GitHub's privacy policy; the plugin code does no separate update - check. -- **No background sync.** Messages stay on your machine. There is no - cloud backup, no sharing feature, no remote viewer. +- **No telemetry.** Source verified: no calls to AppInsights, Sentry, PostHog, Plausible, Google Analytics, Microsoft Clarity or any comparable service exist in the codebase, nor in the direct dependencies the plugin pulls in. See `docs/THIRD_PARTY_NOTICES.md`. +- **No crash reporting.** Crashes go to Dalamud's local `xllog`, not to a remote endpoint controlled by HellionChat. +- **No usage counters.** The plugin does not count installs, sessions, feature usage, channel activity or anything else for the maintainer. +- **No phone-home update check.** Updates are delivered through Dalamud's plugin installer, which polls the custom-repo `repo.json` on GitHub. That is GitHub's traffic and falls under GitHub's privacy policy. The plugin code does no separate update check. +- **No background sync.** Messages stay on your machine. No cloud backup, no sharing feature, no remote viewer. --- ## Your data, your rights -The GDPR gives you specific rights over data about you. Because -HellionChat stores everything locally, those rights translate -directly into plugin features: +The GDPR gives you specific rights over data about you. Because HellionChat stores everything locally, those rights translate directly into plugin features: ### Right to access (Art. 15) -Use the export feature in the plugin settings. You can export to -**Markdown**, **JSON** or **CSV**, filtered by channel, date range -or sender substring. The export goes through a Dalamud file dialog -and writes wherever you point it, on your machine. +Use the export feature in the plugin settings. You can export to **Markdown**, **JSON** or **CSV**, filtered by channel, date range or sender substring. The export goes through a Dalamud file dialog and writes wherever you point it, on your machine. ### Right to erasure (Art. 17) Two options: -1. **Targeted deletion** — the "retroactive cleanup" feature lets you - apply your current whitelist to the existing database. It shows a - preview of what will be removed before you confirm with - Ctrl+Shift, runs in the background, and calls `VACUUM` afterwards - to actually shrink the file. -2. **Full deletion** — close the game and delete the - `pluginConfigs/HellionChat/` directory. Next plugin start will - produce a fresh, empty configuration. +1. **Targeted deletion.** The "retroactive cleanup" feature lets you apply your current whitelist to the existing database. It shows a preview of what will be removed before you confirm with Ctrl+Shift, runs in the background, and calls `VACUUM` afterwards to actually shrink the file. +2. **Full deletion.** Close the game and delete the `pluginConfigs/HellionChat/` directory. The next plugin start will produce a fresh, empty configuration. ### Right to portability (Art. 20) -The JSON and CSV exports are open formats. The Markdown export is -human-readable and machine-parseable. Nothing is locked into a -proprietary container. +The JSON and CSV exports are open formats. The Markdown export is human-readable and machine-parseable. Nothing is locked into a proprietary container. ### Right to object / restrict processing (Art. 21, 18) -Adjust the channel whitelist or set retention to a low value. Both -take effect immediately on new messages; existing data needs the -retroactive cleanup to apply retroactively, by design. +Adjust the channel whitelist or set retention to a low value. Both take effect immediately on new messages. Existing data needs the retroactive cleanup to apply retroactively, by design. --- @@ -218,37 +124,27 @@ retroactive cleanup to apply retroactively, by design. | GitHub (Microsoft) | Plugin distribution via custom repo, issue tracker | Whatever GitHub sees from any HTTPS request to a public repo | | | Dalamud / XIVLauncher (goatcorp) | Plugin loader, font subsystem, repo polling | Whatever Dalamud reports for itself; out of HellionChat's scope | | -GitHub and the Dalamud/XIVLauncher loader are unavoidable for anyone -playing FFXIV through Dalamud at all. BetterTTV is the only third -party HellionChat introduces on top of that baseline, and it is -opt-out via settings. +GitHub and the Dalamud/XIVLauncher loader are unavoidable for anyone playing FFXIV through Dalamud at all. BetterTTV is the only third party HellionChat introduces on top of that baseline, and it is opt-out via settings. --- ## Dependencies that touch the network -For a full dependency inventory see `docs/THIRD_PARTY_NOTICES.md`. Of the -direct dependencies the plugin pulls in: +For a full dependency inventory see `docs/THIRD_PARTY_NOTICES.md`. Of the direct dependencies the plugin pulls in: -- `MessagePack` — local serialisation, no network. -- `Microsoft.Data.Sqlite` — local SQLite access, no network. -- `morelinq` — LINQ helpers, no network. -- `Pidgin` — parser combinators, no network. -- `SixLabors.ImageSharp` — image decoding (used for the BetterTTV - emote pipeline), no network on its own. +- `MessagePack`: local serialisation, no network. +- `Microsoft.Data.Sqlite`: local SQLite access, no network. +- `morelinq`: LINQ helpers, no network. +- `Pidgin`: parser combinators, no network. +- `SixLabors.ImageSharp`: image decoding (used for the BetterTTV emote pipeline), no network on its own. -The single network call listed under "Outbound network calls" is -written directly in HellionChat's own source, not delegated to a -dependency. +The single network call listed under "Outbound network calls" is written directly in HellionChat's own source, not delegated to a dependency. --- ## Changes to this notice -If a future release changes what HellionChat stores, sends or caches, -this document will be updated and the change called out in the -changelog block of that release. The "Last reviewed" date at the top -tracks the version this document is accurate for. +If a future release changes what HellionChat stores, sends or caches, this document will be updated and the change called out in the changelog block of that release. The "Last reviewed" date at the top tracks the version this document is accurate for. --- @@ -259,6 +155,8 @@ For privacy-related questions specific to HellionChat: - Email: `kontakt@hellion-media.de` - Discord DM: `@j.j_kazama` -Security-relevant findings (e.g. the plugin storing or sending -something this document says it does not) go through the private -advisory in `SECURITY.md`, not a public issue. +Security-relevant findings (for example, the plugin storing or sending something this document says it does not) go through the private advisory in `SECURITY.md`, not a public issue. + +--- + +Maintained under **Hellion Forge**, the modding and plugin line of **Hellion Online Media** | Bad Harzburg | [hellion-media.de](https://hellion-media.de) -- 2.52.0 From ecf1e93a1bb6cbb973586d4fb1882373aefab3be Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 5 May 2026 19:49:26 +0200 Subject: [PATCH 075/169] Refine language in SUPPORT.md for clarity Updated phrasing for clarity and consistency throughout the document. --- SUPPORT.md | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/SUPPORT.md b/SUPPORT.md index 815bd59..03a15c8 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -1,8 +1,6 @@ # Support -HellionChat is a small hobby project maintained by one person. There -are a few different paths depending on what you need; please pick the -one that matches. +HellionChat is a small hobby project maintained by one person. There are a few different paths depending on what you need. Pick the one that matches. ## Bugs and feature requests @@ -11,43 +9,36 @@ GitHub issues, using the templates: - [Bug report](https://github.com/JonKazama-Hellion/HellionChat/issues/new?template=bug_report.yml) - [Feature request](https://github.com/JonKazama-Hellion/HellionChat/issues/new?template=feature_request.yml) -Please search [existing issues](https://github.com/JonKazama-Hellion/HellionChat/issues?q=is%3Aissue) -first. Duplicates get closed and pointed at the original. +Please search [existing issues](https://github.com/JonKazama-Hellion/HellionChat/issues?q=is%3Aissue) first. Duplicates get closed and pointed at the original. ## Security -Do **not** open a public issue for security-relevant findings. Use -the private advisory route described in [SECURITY.md](SECURITY.md): +Do **not** open a public issue for security-relevant findings. Use the private advisory route described in [SECURITY.md](SECURITY.md): - [Private vulnerability advisory](https://github.com/JonKazama-Hellion/HellionChat/security/advisories/new) - Email `kontakt@hellion-media.de` ## Privacy questions -Specific questions about what HellionChat does or does not store and -send are covered in [PRIVACY.md](PRIVACY.md). For follow-ups beyond -that document: +Specific questions about what HellionChat does or does not store and send are covered in [PRIVACY.md](PRIVACY.md). For follow-ups beyond that document: - Email `kontakt@hellion-media.de` ## Quick questions and casual feedback -- **Hellion Forge Discord** — community for HellionChat and other - Hellion Online Media plugins/tools: https://discord.gg/X9V7Kcv5gR -- Discord DM `@j.j_kazama` +- **Hellion Forge Discord** (community for HellionChat and other Hellion Online Media plugins and tools): https://discord.gg/X9V7Kcv5gR +- Discord DM: `@j.j_kazama` -Bug reports still go through the issue tracker so they can be tracked, -but a quick "is this a bug or am I holding it wrong" message is fine. +Bug reports still go through the issue tracker so they can be tracked, but a quick "is this a bug or am I holding it wrong" message is fine. ## Upstream Chat 2 issues -If the issue exists in upstream Chat 2 too, please report it at -[Infiziert90/ChatTwo](https://github.com/Infiziert90/ChatTwo/issues). -That keeps the original maintainers in the loop and helps everyone -who uses Chat 2 directly. +If the issue exists in upstream Chat 2 too, please report it at [Infiziert90/ChatTwo](https://github.com/Infiziert90/ChatTwo/issues). That keeps the original maintainers in the loop and helps everyone who uses Chat 2 directly. ## Response times -Weekdays during European business hours. Weekends and FFXIV patch -days, replies will be slower. A few days of silence on a non-urgent -issue is normal; pinging once after a week is fine. +Weekdays during European business hours. On weekends and FFXIV patch days, replies will be slower. A few days of silence on a non-urgent issue is normal. Pinging once after a week is fine. + +--- + +Maintained under **Hellion Forge**, the modding and plugin line of **Hellion Online Media** | Bad Harzburg | [hellion-media.de](https://hellion-media.de) -- 2.52.0 From af5f4d380a7c684de8e2ee26a10fb79b8ce164f3 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 19:51:29 +0200 Subject: [PATCH 076/169] =?UTF-8?q?feat(config):=20migration=20v14=20?= =?UTF-8?q?=E2=86=92=20v15,=20removed=20legacy=20theme=20fields=20and=20Ap?= =?UTF-8?q?pearance=20bindings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HellionChat/Configuration.cs | 19 +--------- HellionChat/Plugin.cs | 19 ++++++++-- HellionChat/Ui/SettingsTabs/Appearance.cs | 42 ++++------------------- 3 files changed, 23 insertions(+), 57 deletions(-) diff --git a/HellionChat/Configuration.cs b/HellionChat/Configuration.cs index 557e8cb..d874a17 100755 --- a/HellionChat/Configuration.cs +++ b/HellionChat/Configuration.cs @@ -34,7 +34,7 @@ public class ConfigKeyBind [Serializable] public class Configuration : IPluginConfiguration { - private const int LatestVersion = 14; + private const int LatestVersion = 15; public int Version { get; set; } = LatestVersion; @@ -80,19 +80,6 @@ public class Configuration : IPluginConfiguration // ChatTwo users skip it because the v6→v7 migration sets the flag. public bool FirstRunCompleted; - // Hellion Chat global ImGui theme — applied to every plugin window in - // Plugin.Draw. Default ON; users who prefer the upstream Dalamud look - // can flip this off in the Privacy tab. - [Obsolete("Replaced by Theme slug + WindowOpacity in v14")] - public bool HellionThemeEnabled = true; - - // Window background opacity, 0.5–1.0. Lower values make the plugin - // panes more glass-like so the game shines through. Default 0.5 - // matches the maintainer's daily-driver preference; users who want - // a less translucent look bump it up in Aussehen → Theme. - [Obsolete("Replaced by WindowOpacity in v14")] - public float HellionThemeWindowOpacity = 0.5f; - // Use the bundled Exo 2 font (OFL-1.1) for the regular plugin font // instead of whatever GlobalFontV2.FontId points at. Default ON so a // fresh install gets the Hellion typography out-of-the-box; flip OFF @@ -336,10 +323,6 @@ public class Configuration : IPluginConfiguration RetentionLastRunAt = other.RetentionLastRunAt; FirstRunCompleted = other.FirstRunCompleted; -#pragma warning disable CS0612, CS0618 // Obsolete-Felder bleiben bis v1.2.0 als JSON-Safety-Net erhalten - HellionThemeEnabled = other.HellionThemeEnabled; - HellionThemeWindowOpacity = other.HellionThemeWindowOpacity; -#pragma warning restore CS0612, CS0618 UseHellionFont = other.UseHellionFont; // v1.1.0 theme engine fields diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index 2b58150..8a2f153 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -247,9 +247,8 @@ public sealed class Plugin : IDalamudPlugin if (Config.Version < 14) { Config.Theme = "hellion-arctic"; - #pragma warning disable CS0612, CS0618 // Obsolete: HellionThemeWindowOpacity bleibt readable bis v1.2.0 - Config.WindowOpacity = Config.HellionThemeWindowOpacity; - #pragma warning restore CS0612, CS0618 + // v1.2.0: alter Opacity-Wert wird nicht mehr migriert (Field entfernt). + // User die direkt v13 → v15 springen bekommen den Default 0.85. Config.ReduceMotion = false; Config.UseCompactDensity = false; Config.ShowThemeQuickPicker = false; @@ -260,6 +259,20 @@ public sealed class Plugin : IDalamudPlugin "pick chat2-classic in Settings → Themes for the upstream look"); } + if (Config.Version < 15) + { + // v1.2.0 — keine Datenmigration nötig. Removal der deprecated + // Theme-Felder ist reine Schema-Bereinigung (System.Text.Json + // ignoriert unbekannte Felder im JSON, daher kein Crash bei + // Configs die noch HellionThemeEnabled/HellionThemeWindowOpacity + // serialisiert haben — die Werte verfallen einfach). + Config.Version = 15; + SaveConfig(); + Log.Information( + "Migrated config v14 → v15: legacy theme fields removed " + + "(HellionThemeEnabled, HellionThemeWindowOpacity)"); + } + // Hellion v1.0.0 default tab layout. Five thematically separated // tabs: General catches the immediate-surroundings public chat // (Say/Yell/Shout) only; System absorbs the rest of the technical diff --git a/HellionChat/Ui/SettingsTabs/Appearance.cs b/HellionChat/Ui/SettingsTabs/Appearance.cs index 5cde2ee..ba07ee3 100644 --- a/HellionChat/Ui/SettingsTabs/Appearance.cs +++ b/HellionChat/Ui/SettingsTabs/Appearance.cs @@ -45,32 +45,11 @@ internal sealed class Appearance : ISettingsTab using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) { - // v1.1.0 — Diese Settings-UI wird in Phase J durch den dedizierten - // Themes-Tab ersetzt. Bis dahin bleiben die alten Toggles erhalten, - // damit die Settings-Seite kompiliert; sie schreiben in die mit - // [Obsolete] markierten Felder, die bis v1.2.0 als JSON-Safety-Net - // bestehen bleiben. Das pragma unterdrückt die CS0612-Warnungen - // gezielt für diesen Übergangs-Block. -#pragma warning disable CS0612, CS0618 - ImGui.Checkbox(HellionStrings.Theme_Enabled_Name, ref Mutable.HellionThemeEnabled); - ImGuiUtil.HelpMarker(HellionStrings.Theme_Enabled_Description); - - // Clamp 0.5–1.0 stays consistent with Privacy.cs which already - // shipped this slider; lower values would let chat windows - // disappear behind game UI. - using (ImRaii.Disabled(!Mutable.HellionThemeEnabled)) - { - ImGui.SetNextItemWidth(200f * ImGuiHelpers.GlobalScale); - var opacity = Mutable.HellionThemeWindowOpacity; - if (ImGui.SliderFloat($"{HellionStrings.Theme_WindowOpacity_Label}##theme-opacity", ref opacity, 0.5f, 1.0f, "%.2f")) - { - Mutable.HellionThemeWindowOpacity = Math.Clamp(opacity, 0.5f, 1.0f); - } - ImGuiUtil.HelpMarker(HellionStrings.Theme_WindowOpacity_Help); - } - - ImGui.Spacing(); - + // v1.2.0 — Legacy HellionThemeEnabled/HellionThemeWindowOpacity-Bindings + // entfernt. Theme-Auswahl + globale Window-Opacity leben jetzt in + // Settings → Themes (eingeführt mit v1.1.0). Hier verbleibt nur der + // klassische OverrideStyle-Toggle plus der Bestand-WindowAlpha-Slider + // für das Chat-Log-Fenster. ImGui.Checkbox(Language.Options_OverrideStyle_Name, ref Mutable.OverrideStyle); ImGuiUtil.HelpMarker(Language.Options_OverrideStyle_Name_Desc); @@ -79,16 +58,7 @@ internal sealed class Appearance : ISettingsTab DrawStyleCombo(); } - // The Bestand-Slider WindowAlpha targets the chat log window's - // background only. The Hellion theme opacity above already covers - // every plugin window globally, so the two sliders fight each - // other when the theme is active. Disable the legacy slider in - // that case to make Hellion theme the single source of truth. - using (ImRaii.Disabled(Mutable.HellionThemeEnabled)) - { - ImGuiUtil.DragFloatVertical(Language.Options_WindowOpacity_Name, ref Mutable.WindowAlpha, .25f, 0f, 100f, $"{Mutable.WindowAlpha:N2}%%", ImGuiSliderFlags.AlwaysClamp); - } -#pragma warning restore CS0612, CS0618 + ImGuiUtil.DragFloatVertical(Language.Options_WindowOpacity_Name, ref Mutable.WindowAlpha, .25f, 0f, 100f, $"{Mutable.WindowAlpha:N2}%%", ImGuiSliderFlags.AlwaysClamp); } } -- 2.52.0 From 4a613f7acb8454ab5b0275d67efc45c7b69dd33f Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 5 May 2026 19:51:51 +0200 Subject: [PATCH 077/169] Update NOTICE.md with maintainer details Added maintenance information for Hellion Forge. --- NOTICE.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/NOTICE.md b/NOTICE.md index c8ba530..786f887 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -87,3 +87,7 @@ this file, the README). The Hellion brand is mine. This file is the canonical place for "is this attribution correct, is the maintainer reachable, is the relationship to Chat 2 documented". If anything in here is wrong, please open an issue or contact me directly. + +--- + +Maintained under **Hellion Forge**, the modding and plugin line of **Hellion Online Media** | Bad Harzburg | [hellion-media.de](https://hellion-media.de) -- 2.52.0 From 3e98b9103f8c68442a8671f638722a648bfa63b0 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 19:54:56 +0200 Subject: [PATCH 078/169] =?UTF-8?q?chore(release):=20bump=20to=20v1.2.0=20?= =?UTF-8?q?=E2=80=94=20layout=20refresh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HellionChat/HellionChat.csproj | 2 +- HellionChat/HellionChat.yaml | 48 ++++++++++++++++++++++++++++++++++ docs/CHANGELOG.md | 18 +++++++++++++ repo.json | 10 +++---- 4 files changed, 72 insertions(+), 6 deletions(-) diff --git a/HellionChat/HellionChat.csproj b/HellionChat/HellionChat.csproj index 1771783..96cfba1 100644 --- a/HellionChat/HellionChat.csproj +++ b/HellionChat/HellionChat.csproj @@ -4,7 +4,7 @@ 0.1.0 is our bootstrap release; the underlying Chat 2 base is called out in the yaml changelog so users can see what it derives from. --> - 1.1.0 + 1.2.0 enable enable - 1.2.0 + 1.2.1 enable enable - 1.2.1 + 1.2.2 enable enable - 1.2.2 + 1.2.3 enable enable - 1.2.3 + 1.3.0 enable enable - 1.3.0 + 1.4.0 enable enable - 1.4.0 + 1.4.1 enable enable - 1.4.1 + 1.4.2 enable enable Xat&l^6TcG_3BSu5K1d~^ofe0>KVV~>Ut8a}?)vRdRjhW+SmRpeZCcK>i_>jN*<>G%DwIlrWWu(I zk`fstV7p)$gDpWC8FgR!A2;sN={3Rw$T$ zJeox`U{N!7cUl)$rVoGfO}RebYB-SQ5pUT1@{2xmkpXj%6$p|6b180pwvolYjO;r4 z#MkeB_U{f27a=f+eWU@DO&6Rmc1R9`x{y{s?#~uqb&q?-h}ADeM<3P>RI?8OS(2ki zM*%>Ri0waTKaOy^x#Cz*urqgWnJ*lYn=7U_c8pIQAVEs0uv;r6m!SJ^pBK|QJ?kU= zvW$N33Xsujp1^EnC~}%`)X2sZH@RJC1|f#%o0R8S?LrNy?G@CaL_`KEHRnx1NvTze z08^DJ<%)iN*0m+bY)H%zb4PSE-v!7VMwR?GKW|!od7Y!z0rc0ww&>J8_I}xwrt}Q~ zO)Vu8D5?=T5E`PXPKOeTa8_zmBvXy7#K<+20k{ZbqiR_;lP`WL`8$v0e3RLK{U_}# zdl?h3aym5LO)igEJ(A`4HEUGEbY&C)222E_*>HA2gS(IatM?37T$#bX2B*4DeBJgJ zyy_wobJmd#5cdWQ0>mY;Kp2@l3G86<32*S@elml*5(tTX1%OajxWK9i4n+;jiTl5O z_^O}D;9_+2n04e~T{2MEmk4R))K~x*EZjZf`~MAlvcN7^T-^$R5vS_T>05dbN1FG- z`1nm`KPrK(QYx2V<#QRW#mbrOCoT`8-@5`NW{;;^uT5a9aiWxz?p>QUqC`6TQMa(& z4ZD3yAgGJBn6}SsBB-uIBb*^iSel z`KFg}>s<=zTk*p0oh+)9Q50h343}>Sa`Z@z;}2<}45P_Pp}=&KKEc+1#d%zrA`-e}8krx{8Xjw+TcLm6FLacW~ zq-gpIRi(OV9F7_+$WpUwqVpg(iR>an(`156sWoKR2t^YfD50iXo3*7xAi$Q_M#KEh z!{mon>L~Azj-9IkI8C8)-A-F3)3z;?V4*^|aU0YyyOU@Jl2V3{YnIYTLlRYlDG=Bm z3bG{=0K&2iO5sXKSW*lIfYRUZoolBrJdw`Fzdk(ELP!XWcNSWkS1yN95UUSl(NSkd zx_}e6{FAA>LS4kxj~=@PjhnN*10KA4npb`7n0*Msj$;q|ANA{gp;RtpQHzp&7$hN# zxHOVLve}cU4GuVQ&D;5!i)`jKfHC_MR_?^rPvd+qO%6rK2Fioy??x9rlD_)-$7}WD znnY#owAqIN*Vs7y9oK(709aVyqwM9)WEQitbLQV21sVQ*ZEq6YRY%{pZRhCZ4ZYTe zUG@*Oe!PJj_`XLPJJr5skNBT9Ltzrw$=a|N(*MJ)j3DZ5>X z$o%IV=ZW9-%j2bd=vKK)X0L-Nc3BYD=ro-6MLx%TSw;thd&6cPO zf8$7efZ=%@3WY%S!}^<^{;G={DBOML6EuD3)YXkhvya+HQf@E&!siRX0LbjA2cYDl zUu=AA0)V&9zHD(@Ods^NTO%|%`CiI&k?bRc$!_@PKgB`$q}+Mv8jUv||LR9A%jox> z06ALRt`(OUYLk_wkyEb8JfuN|Din&|5rWz|0c8?hdT=ACG-bV8?P4s+0ANn}_UJh7 z+oMC64_=htNqc37D>hyQ-dKnAV~^>hZJTEGSV>DNweM^@7oBomHm7B9juwpC9Lhi{ zqgiyavl&7rtuO+tu$zGZj2#(CfNhWsiA)IrGEH@27=Ou6a@Rt7G=KhG!!_?n1agkL z+N)F!-|UYbAE#F7V7wl1;%leh!3TJSvp;z=vFhHz^r}Vw%y+O4rDRY%WO%n;v%!Vh zlpHZ2_F@njU@nmbz=3KYAer*fo14%0>AUS28*G8umoIqD!)GuKh>!b|TmQGaA4wlw zyUzGQN3GUCn0;iLFw_g<+3c-Fp-`30IYPPpf8IiKznw(}(_^>r#)oaPr#h6~l?d`@ZA(`qFakK?p*$_unsbNA!F+H^`yEWH$Se{S zilYp}5ZFb1j>2{Q45f-%7LAQf{^{aZhmNeNTicCzrYjDD{PkZFpLShIkO7D z`ued)McYJkRAJRHpc;y3h<@k_GKtC}JTFb5iJGJNqKxJ#OJ!Gt8f8RQF-VfZk`+P( znI$8tOki7B0uYvLiYtwi5KaV9O+M%?{jYz{F6Z&ew|&2k4BItoF`msmn%%}B(;L5n z*|?S=S4P&#NpTUr;)WlhX?mmS`YkbvWG{iSuyFUtH}wDb_hwssfHR#)2W)I+F44oA z0Fc;+0cv0jlSlsX{P90=!Je@YvMdfvTpqG5;Yc(7`2W81c|Utz)Rpz4*<5>nLlCZQ z#K1l*gohNDNTiUm1{XSd>vw$r=+q6yv+c8wA8jRjC?%`eiBgk;I(a4S5uByM>}-GP zhGiN3-Vq?f{lnAo&;-sQWTGb;dqn0i3(_$JZBT1ltAeJC*Vfvmk%R%@s)dJn`_1k~ z{tgazgVhxqaE2aMS?7-E7@b0h1SDlZK!%V7Nkp;xRE=?1eK^~YvnXn!Y*nh8S{u0* zh7PI#urLyJzzKn6Kr&b$350`eNQxa~K-dtY&-k^?{#Tz|IZtW+{M+M-x0(z|rQ7B3 zJ4^L%C61ELGPc*BD=x}MRyJsc8tAGsdE;b@z04WO#wJ*K)NAtzum1Q6=c@ru_GB<% zBymY(;6NwQGrN#wDOvlNH|E#zR_)J4-CosDXl2rR%pv0KXEXL!*UbXz}kYevd+)9vTH>j--jvQb28@~}58zUKeV z;2g`s0s1FTc|rvB+SWdPt4 zUrwBOxC|ntA{6W+QXyCbm!t=0n|<2c&53XRA(z1=rrqw7_AXJ>$YG}|WG@bA`!7-} zWR#9C20Gj@u%n90J3Moif_> z*WDQ%@?l&I`ftK6@wKdLm*w#Px(7D5OM030?FGO7}_s&y@vp*A$pWLvTd zIirqpD3**vP9YVnpq5Nh(x7bY1lN>P0Fp^C3X_N-!p>rB*|Nbx(Vn4<(Eb4QQ-oU}NBvloOyNg?&`a>>#g>FER+_PfkVhn0~4(N)j znM_g%X*_D`vW$MO2#^>*o_4KqO=l2MMGZw3oMhKV8J${fCyjzTIoGCrdD=_AE+>t7 z9)Q7zu)*q%=s0KnL5~@tZHrFIDX>%#mSls&Ss;v^0mZW9Vz+^Z9O}qJlXxUcB^(Yn znY|`dG036FT4YC(Da%=~oZQKh08$c;1j&{tGNy_YM%=;7zUd{K?R1;-t$f>yeC5ty zA#%FI=#V>0)tjT4i3KH-D+~AR**|mx2z30mVSSgq%;gM$L>Bo$!@H|aA2oMKsep|S zR!=bIQdtO#JsFS?fGx*E`&;s^O*alU9~&@$xjYh(lClu!!CUY3(^oeWj=biJSp9?o zfB{uiSN0K($?lNwuO81ON!PRjb9ZZ3RX2lGraS&`4{W$+)#S{cZ7w*V;jN#mDQ;AZ zP0SXKwqH)xAMpXQxT<(124j;bu*=YRh*C!5xT5VwSpoozPq{Bo%j5G&^BfN4ftBLU z=okT;jEVVgm{5ZIOkLzBf>aFJ?bOX27nN)FKus;Q!s zB|?Q1cF7_+agqVfKqQfv7zh9|GE6v;v1Kq$iLgP&vMm8d^#N}i|L$k*avn8*{+!+W zy~B{<+m|4(`Q%*=(c15*Z7T(Kn(rTT&piIhAFu$*c<1ywd+HF0kjPYd$nH&F{<2dy za0fX7B9grrj0F;xNN|9JLRbQWh?L!a>>D<}@&lWB1-gV66ls$4w3Wl*p+gp3Q8%wR z9-cSn)i<8@6~F&^T7P7h1;(AvZntDpD*KXm2UF{}AHA9LIN8%f1U5$8UD+I}WZaGV z&4IlntvqZwzL$eJd7SYiLveh3_H-6Tr5+iUH)4I{2gvX{PtA4^iHd1^GTgRmPtLkg zkqiRkb9-}3d5#Q#KlnaRulRER%Fd|`HrIenV0Ha-hcb#7PlaUzb^uA6#x)WPS(3;M z5Oz>hnjAurRXvS*Q$lt)0;8m>6%S@yhaw}Yt}0q6I|_;U7mcnouyX(X|&^zQDHn~4`m}AN+{#S z_x{xb*~16~VqBDc#m&QCzj@v%3kf3#_98$6r_H6(!S2GUkzrBVAgiVEC%s|k@xL?M z0xl^;B*6LZ&DTAI^W3^B4o4RybpG;pv!|#i<}v!-ALAhMl(H+z=KgDO zY~^h*ot&k_vW)%_A0WqnXBx6iP@}|(XGf-OTZ#g(;lg7bGWhT|Sp59^+!dF z)UIa%jMA6@LdFf2C5i-=FsUrE!vqQPd;V^(5wv2kP2)F_F-2*v0Ve1JGAwI|KSnr!5~Z{8gmhgKN`A7 z<7S~`Po7VSqD62Zi&uYPyGCYbM6hS75)!gU3ur3)pxQA>&LF!j<${VMAi=MM0QlLNF-TAI4ubeDqs<= zDl%APWRMYMFpZ>&+oY6*LkqQ1jqze=+N^=BP*WG->HuVtkODbK8>f(!5(rXYNfr*J zoKi&?VN1#Y0NKFoi(k5W)vG?6^R2AAIr*MlxdtoS@%Ur!HHYSJii#@5x$|vrVoxjI z`GRo)C`~Nb%bWn=p^>7iX-yvf7sKPvbwjcR7_k=twlJ7WBhZ055{d|m?7IxQd79bB z{(1enU&sIuaNrugtmdP4cN|zC(8&jLXbggWR}Z+gw9G5nee&xzzf_Dr=9pzo3#`3$ zu`?=`#C~jJv$nOFAOMScWWt^VKmh3CYgE@jr08DvP;<`&C9>^{IgmF#mB~zKb~f+# z?v216jkcdKUIxfVei40hvZ5mk&Dv22u&_AkVg8LHpF0tFcsE%2we?jStk(^=9}jv| zMAMYZDI(g!yaNN&Ct~(hT7H!%XPrm z5ra!4mK0G#7>BB4J7gk}!?G*`fB+*PQA#8t5Qz{NQUFG1_6cv<(1jQCYUjnzo?r9! zMTFf>H;KtMho_C!>`t_u5Bw-fyhR9C$CsLP3aaEC+_!lf5}f;0AO)Yf|XC3S2+VIg*Y_Iv}>uo zzxtj2P~lzQ)&^5XFl@gXA?ulXe+yA8T}(ZKp6ehvsnoO*@CsvZf(EW zU90B{^T8`^yux4pE9>vQrc&0Zuugf(cC<}2CsVE-9D_qvDzGe(gk*$60wo#`7Ip0C*ikyD3aj*4N}{5a(&-ceG8ACsLVB#@kT{ic`PMoT74KkeOl{)g^Uf8 z892kXIJ-DV11jzcUE|D|S z0my;vM7S1{28RJsDM2DYA`uc67DgBZ$jCCLd*UlMc*PSd=UJ3*{ju%YrU<Zrar z+Z>uo+4V%Tvvp&0!d^y@LmNniy>*zBtp3g4{z4Pob{4MQd>3E+Y5<&sLF~nl1wfcf zB9ITX0s+9nbkF{&)3olPdFcG+`131n;BP}%7J$tmlEPsGW$_}@l7{YqR6&dzbn>fK zIy7q)rd0OftVIY@0VgO`N%myG6u`;F4U9s~AXCLXacLQ?MnkCK0HP3+c2UX%u9$2{ z5w(`t8IQM@W%Q5q0AZBhy7gnvdVh302av&sQzc(N$_8*X!1{IlP};VvtXtT2A8mCS zFh*FEmXNU$AzTZVEsPSdWw0zLfB`Ik6n13V3(0k4s3zu3)R+ei3y(_Ahi0;f5bl4Pk(rbsJsbx}VwvI~qdYjh1^szYV45ScL)utK4#U??U* zV37hWW62J42MZJ!kcEW74jCDe=~uplxUj>mc3$x9E4raq6J^#@77d4`afuYWxnMM~ zrz1lFk+tKGAI%=JZ$U5s-PxZyU5e+pdH5fn`O07al(PUrm^~SaEM&yrj@gqCAcKJE z_DgT3v^|VI?)AG*`pwxTjA#h})BwoBcu_el#w5Z4bc-K5O&M<+P-@blqtBWGu)T_a zWFJFjN}$o&D1$kpIjWoA$DR!A*`)rXuFcjMO7_7XDs{`G4q8IbQ3DA+wXL)Nb44Xb zv9i7SS<5o|M`nN=z9`X^+xO;vb$dNrcRjzh4o@0N(=;nbB-tWu*&r8OsB&XTq$)H{ zA_-%{SpWehEEMXI00Sm4{m4iLyEv+xqVzG^jZ9O9X0V#kP{P?N37MuJxkmJWN)?op zX)-f_z$W_$2^+AGWsHCXj6fhL#zr6vAdmrs{7pZ@?uCW)>EegKX{~+7kl|(}e|6xH zNDTsc?E0*gE%tKkFZ*chP&4?W%pQV301dFZ^%J+!c#g)8e8bK+{?5P;gQD?WcpoBHAn2i)|dcwbxnpqyDYg98qEh z-t$1pChk*1(-Pg#EGO*ycz~4nozI&V$E%*)!UOpO06sNJ88ycnl!UQmlePk~7H5NH zk;N$N6bVTH;FMAhY9QG%0Fp(50&t6DBnwHZQG`QPRYPA&iQ){DYHKPrH0oO~N~+4) zMztF$vx!uIO_dFZ6qb;{281u{kd%R{jAa90$rubWZWhN*kJa{kfZMk|=OvT1+vK4= z(r+KB z0fj) zgl4*BJnrPl7dWtmYB_=KI-4s1a##F_Dfoq>@csVfNs`Hr`#d z?8{fIGalQuf$w_=F#f)8ZDU}&DRuSaa=yOL28g1Km=^yYS6|O{9Q^~XXxGtD z#Z~+b#~&;#WcH;1h6P{#QJkj)hr|FTxYP7~2|_dR7|!JfkZGWj>_ZAdIp6&HkDGCx zmOZr?LCGMwyXg~!YxJ%BJr89MMNK1+Uhv)Q>#E=XU&;zKnwfa00ljsV@fo#Dx9cBY zJzmb(_n81WS0(@H_4UWi;Cg0Ue_hX@zu!IKcu`jdnu=}OA;W-d6%ZJQWxEf%iv+nK z*%+!UNbZm!5`mEj+awuEP@*KA1&3uBA`q#Ba8b6outv_ztfIEfHY<2ggdK=p*cn4r z5}{g4*aR}J#Wo1AVIT!E$ifPN1O&2?!C+FzmPre+Jh^)zebRr|znra>y72#boI}(9 z)k-09d)$=y=f8`4rmL<|cQS;d*k&(7mq<2+JAB8@KU^9XsW^E|A#u>#ctZl4OCk(H z(m^U07u<;a-2dxV3TegFSI__g%zgxb(+}>rENp@KfZ5i+y_J#d6T4S(r*||lt}&uz zKeYhR&xb1(T$0R?gt@y?O{^$v@}i}q>#wUP)QVE35;zd+c7xO zN*P;93sf8cAuPZ_hCD<#C8bb;F*0Ov5NQh;%krE_qz+jUWR*}G#B-;iH3@L*JgxmTFZyDdEklyy+@k?jm zpRKHjL!&<_B6o?|_^f{W?*;cnulSmnhCmH$Udvv(MFV!<2X~NXyEI-HH2}5_a?H;{ z*oaGGKp?XxVc+H2Mb#H3jeAhT9VBreQ4KPefdhp20ME|;yVHm(H=hUR#7ZeZVLz#% zxS-%~ML-pIF@4jgmogNZwy7Iq_A(S0C^q}*o^+(F7Vxx|;}Zb-KUyf2Dw7>A%joy| z0ErUaq};wY7q}j1*`T^oqb4L6yL1FfxQm zkY!miDu75usz5`vWD#l&7}=1L!F?2}qBe@~5E(-*M!}E^2aCuC$&@}y?ZZ`Vig0v^ z9qywV(pUlnWExwBFb2tjk~YF|;Y2170zesLe8wwJkI%F{=V|M=Zjmc19H#q!{9olT z4%OL4|Fj#pCpy_Wjt5o!K!xRdJp4^S@>0J{KjkX=}jGM7w<{TYM>hI5+v zkKN*ip7V%(8Au~850S#c=O@T}|HCal{v{{5ldwR}MY10c)s-BE=W@wx4K{_lD0(!b zF;ZL9e4^M(Q@Xx!aH$U(FRq~_P z!PBq598RM`RnU`r(lfQ%#zPC zM)P}Yo&I@oWGw?Cd}5V7#iBDx({hJ%2dKTUzy#m}T{MzhI*S7lND`Jg$KBbNo~H4@ zoh;132y>a(AV~26SHAb>Z+hw}&M^RKI`$_SlLME?*bYo^SI0*#YmL*z{M)1KDbj_V zG5gCSp=pzW&#ph^A>85WfAuYWTV;(GWU0$J``#8HiZ)*HWw&2L@;Z3R=U)%dKc+_8 ztu|%E?&fqNS0kqiO{S8tBW(>*VyAJCD6u6BAWSSIY~e^Ouws;W)Fi3l2ul?@_ME)<2NcMfDadRc{3fHI>Brxt+_fw0NINn!(FKwxEL zRgghIRX7br2n-l3?vx0RpYRL(()MGy3v;+8H!IpQ#_frXVa?fdG3AfHEV`=$6isn9 z?9B)$nE-I^@B$rkTYv$ZePm$azg>Ws{TOU4F0gX@fx?c#x`Z#dfXFbHOJFCM3lgN9 zFy{yvf!UvIoETkFOIsddRgu-^u2x?26HSJyw;pk7fa@Th1?J1ZS+C>?4?PK%I>5Ur7p!64USFs5XtiV-pffPl2HE_kp2t|*d72yjA4S119%1t+d8 z11nE`IWgVlGQ0XMFVgfzW~?25&oIl4%H74vmq;hqQ$j`gO=52&z>u70#wBNC$iNB!mPK!1)IXZ+6GPIXd|Lzn6(E?5JGmO3q^d8!gzMts)|R;PG6F z&3O}oVa{Ex{^94hUNco)>t~19OC7RbNcIc%FQI=?j>q&l^2W|T>SWrsZ~OLaH*GB1 zcKdPvX*p$ocnSX2rms5n|NX`r{@b6tX6eMDZ7LKz$p(>G!hg=QxqlClqiiA1;8K)3{FUZ z1c(d?(%ivzwT*-TNzH>8|N5BAY<_}hS;@i2kHE_Mz}>WebQG;!BgyK5<7@0~F~@+! z`BuN?C^RjQ2Wt0J5MU!NnE=Gz47%9Ff92U3E(HK2n*$P;Nfd!a5%)-#A_bkqzFYwn zj&gA7am7ZM3&fE>JbL}kv5~78<10Uzy$F)hjLGM-kM0P~3hLzP$YALEmiBtOQK)3h z!cQ8ObN0Ps1yWAE{EFXYt+G*J{ZY}3%INAzQ+t;zKn-G{uoKw>79NU#a3V0up+!2BZn7R6(To-la*^ARt8$lqMh`O}_lT zKknQ+_kMreGdpKzcF(hWc4p`7p7+`JdCiGiclfCL(=6m<8_IM`jC(152RuB=dcyf6 zuvW;w@a3}wbvcyB=k^-nz|eDBiFeaB)0PX{MIsFQl1UYDwe#qT2UR&WyVrr9KTS~g zJKS!3DT)zNL$O9?-M34gq!wHcu}#)84OQ->6ELNvLY(#c@TO7%!APHGbRn8;7)1@f zRvhyhVo|~UdMI5dLU-$BIsV{u!#yI=*t@v z#9qy!B}prik70n_;S?0q+)$EgIDm)bl889zTtrwTPOP6R{zLPMmd)%wzPdfOlktJ9 zHAUlcljn3H*K4RYXu)pu%YY8gnh$?s4wlOcIw&lg=dPkpjBbLXW7{C0PPnKh#%u{o zAFwv%JAeL{=ZF7zL3r2r3 zBQ{#gt@bvqn0p9_REjWI@LEsexv}~XxlRnc6Jj_dRi~-pB%~{4SapMJRzLeDH)BjC zo5CHR$6DZ!?wd>i32aj(dkIO=R)@S^E(vKJl_LJ*2lFMUqxN9d()6TlF{ppQS!9f4 zJ0BJ4DSrR+cdf&MZT;ide{}Qq%vS#N6cshUjGb{8+!E{JY>Zo9_}x?3XsRu6GeF!7 z@Wj{PR#N+#>h2i;e|)jk5twt-rZe$L)xo~=`|W+ds7I0Hm2%1DEe9Q~+p4~Suh-j^ zw$!~Kv<&$ddU4jh$_$R->ldfkp$Iq9DeFj8ft;YA{SOqVd*=arS%(RSH*&U6@fA4c zkMvda8iFR$lEkUKs|41*1j_ErD?f_3SIc4OH#6JINnA}$Id!`fz{&bNrhB!jrH1Mj zu1S%PtT@J25YYr-fiNY;C=s=SREK0;r!o`mlA&Y(Z+h47iAbOwoWv&BU`*q`Z#5LCNrKhZn`u7=WFoR*=5gJQ2#|m z4dCGcJ=f%SpnOaveHc!-*Q%s_)s&hhE({51JxDJ;3&}faPYNzL3+AUk*HK#f;Lha* zA-9&BeclPL;!@zYat|01&!@yQZQS_0Rm0CHz%&}Xe0P(*?pNb%DStwC*$u@lv*BmF zCTzYZprh9eI&7Ntaoee26J~pcoiy<*SLLIbq;jS*y^U@)O}9E_=?kTL6*_EsH00J$J2 z$H4*$7CW0&;b~hFyAM>sm$&>T*XW=RpY+5@D?eZozI~&(;cK%JOK@{hk~n%KanW6o zE5+@TdEmJ$CA^UKitpikaNTCVm&G6W^J)VQ|6*OXx8dB)QDej~GC6Kyb|oObsjS-1 z!0-!aK^Q-#dt|m z^nSi5o*+%zYGt424D~wcuV_=)s_RW=(M8OK;`%*mr5=0Zm@p9&8ki^Qs^C`W8UWh8 z(}JnA8p;RElJ~>ygd$PXwnA6HSQ_@YT6;lz1D@orR3L_1Lkb+O{q2?D+kWR$hF7T% zjPHxi7Gh9DOgH3?dS5?^+5-=u1rrK@B@owPu4qOZa_epo>h7JhUP9AIAofZ2ecw%{ z(AljlNymDhQ}Q&2)@q-^52i&HxV1xf(XEfr(23BN*yV`vrzs#VYv%NDJ*Crag}bq? z-I`uAYyqW=3Ia1*A0Q#nbxRMoH74_?bOH<&O8XT}eDO6)k1B9-j4?pLkq7ZW%n{64 z%OIB}?`u7qU5H0Tg>#2#7i+X#Ufpx>tD@|55S>sFeXe1Ld*4=Zt!QX8;wV2&%(l8{ zPxNy|#6-kgVVn8aHvRay6VgEtcS!O)hu(0}6l9U^=`}+WrXL7@RRcZEpRGX4xNDhp zX4MG@3ZL67+`<+DnGJoir4`bcl)FvDK{~r)tbP#|rLLYV!Nfq4h=SFFDnVhdlChZX zQj|@0ds^fd%x#mG*jFpPgmSH9C>2pU{<;}?I*gy^ikUi!B3v|ZhkFfoVJ? zezgQA4VPQA{wZ&MK6?KS*+8PfM~4In!3dbdG|PJ?v$M!VGR>$)$cwqNA5TW6K82hv zvc6NC?+<~QeU%QNnknk+^0BchA9}nq^n1!p(F2w82$yhxURd+MS_6m3L`q3?C90}D zx5$BwlHWISHJ6p+uFZ9&mA5GB;rXNpRs?bPJv zSlL#e**~~O^?aMf{br7wezHrYg{3fil7^G;tW-?0K}KZ{2r+CO0Y?;q>M@!KH3J0P zO5cnca8+shuuSD~a&cqz6n;7D-&tU@6iA9g zrG{NBiBtTC=ipI6eGh)>TKhCM9P#^ujc$gTJKR={-;)nt%h-B@BIh$tzx0NQ1g;O@dY#N50e4 zeWE#6s3GNPDZwNmDq5}n3o+ASZ}!pISevAxvW1{hn5Rvli*X982}aNrYmcWbao`bl z#bWd26$M;*Laq;{8w=nX{wxo8o;*41dy{P^|8;i0^tv?|@0JMp@E`}g{)+CEdOn^+l$8C+t(Gc?0Caw_t(s+261bKi2 zHf=^N|i7N{RyxD`?bD0L^$h zxBx7yntReeJWjSLv|pyZs?JiMMAK=CPmzRVBL{D5qK6Fe*Q>E=S)jqtIy#!3aY!Xn z!q0?-PnM6@$fz)N72)~yNEhdoZ-E>yd25^LYI&1j?o37R*3vF^?Iz(JZ5fUxqd7t2 zU?TAQ%TiD5X+j^CS+wKzaqGG1kn0zBF&`mD+L5Rtp@mwf{9NKW-D5bv1-DpZ$&XKZ z3x*q!K`w9`kcVaC-Q-bDw~KmyKsfPvR{$>YkYv5btvzbDen<_+=9_`T`RFte+*Mo= z+N_u(yNoO={+1b%!21}M9yED3Szi>i>?36^r@YM)=>3Nj7rp=-KWnuLo#&~$JQ=nP zYMshRI4mZuOF|FMKdxWlH~rqTsqE99)2K2@7G9xzJRrRQwr*Bh1CfT&O5<_%d+^Z> zym*V9Pt%2hCAQ2!9h{8f^?m9~Yu)K-!k>(P{MH;KUIKTujBrghDgv|EY04xEhH6zA zg@83=0N3mQRAJ;Gk8A3thH2vC*4=43Mw1I5|CU!>H@)Ma)BVWkN}-z>ukTvL_=})2 zXNV7QR7O16BaddT)lCQ>G(O7j?|r}1buafKWkO`~5n)zr(h1#LKmafG{+bc`_zhpA zL4({A_4Tb7*!Bys76ZSJr=IS9>0C-t^2Jsx!Qnb`HB^a8eF{X7oF8rCZ17WbRLOc` zi9QsKvJJ2=1b!j8^hq=okxlMet4u}t0+z_}=+1=TD!nV=-?+krQJM9zT8uHP(SLs! zbLb~>%JlV&u?rm!eiLI?+BfErR8i>g?6%T%n;nhvRzJBHd_YViG3Be25VcG|ow2Cf zL>H)8tzqdl58#yP>6hc+u=?YLV8@2W!Numrv)yb-TjlQR5heC4pt_gnFA^=hIB+5+ zw~*^5X@7=jF+4#mbp;<-@9f~{{pQG)aEePfz?dy|5FbCww4RL{CE9{rfuks|fXPJJ zrZ%R)=<>>oo;Op3?r3TNMOfe7sY#9da(@2+4Ju$uU36zC`|s4~MD>lXpwzn^qsLC?wc`lQjs;O9n4Ky8#FxX|&R8xGhE`eYcu z!5u+1CXR130ui1z6dHqlwGg}enMmUu{gT3Ttx`aQX&~n1!pVf)EhZ$KCqD6P*g zgFRjHIEqT7gFH&Rz4syM2S5E}{NR(elnTQZ;Hu%=-^~LaLmA~m+!!hTY(Y-Xu5E=y-LZ-iDEHRn?%_(Oqh@DD4tkhI@ZhanNW&IR8 zD68=_$kfq)EKO+r+0Lq54Y$pe@+q5*4&CxGG2bAMXF{wiX>Z~^H$%lk-w8|BK8_U@ zyBb+ETrT<`zJ3iGL)&Jr9@~;b>p~-S7Wc&}a^-Hdw{kq)k!5CzZOur+H`*Zo9h_*u zhmrene-!|jm5osSXF!ilfzD}l(}^ECz2woFIzGOKZ(xFuxb35;k6*SoHoeJXUSy5R zY>vdMd~%V=ZmW`;x(`>-yKLFDm;V_1hQymqP}^CAq0V__NhN` zdTA1#S`CAx(8?HPy8n9qAwpzG9gVJb&271yBBwe$nGJbOsud2K)q3)5KkD$!Q+M?b z(b1S9q=IjUdG@*6MTu!GK012vz0OCIw=?E3?))w@&TBig{*yDdFjypr6oupNrVR+? z=UJ@A)~1%&ohbh>O-c)mCJXQWStwiUOf~Lz-oH0ksw}5f1x6X>P1xJQCIGK0!Ao@R zF5LdrL{L(WQ%c$KduH}=zgnV|!4ougp0!Up=sFG9!j9$_FegYY*R^Mxm&5;;%r@6< zi9F&p(9km5?KYAOFV+3^La<3|ogd_=D@?4VG5*5ftL)n$%#cVkWOQ~{XB-XnC++<}rHj~Sq2D9oZk)K`u(lF6L zOQEQRgj+SQ`+6Y<^u$t%Cy&Z@U_CfQIF7O|JbExAJ3s$+kUbR-MJGgkO@H-X5$AR5 zAhQOX;^m*1mjV8d5pgTBf0d5;d3o%Lrhc$Gh$DqEGqK%>E=j^0f2lz4xxK;NsE^yxZ(48E)bA}Y_b{kkuX*>{RORPDTU5Y9%4rFz z-M=}bib`X04?`IB%NC_kSID|<rdy1i8C zNAgT(iJWc=`Dy!{Yjrz_JBB~mziED3<>Y!MyKV`jLFXPe7dg|a!CdwExgpcV%&gl< zMylW|lRrnF8;gCJ-s~2Szx1kzreFvTDu3el0gvu=H4|X4Me%=@ zoQ#cQNG|1HHW(Q9F6i<5tbKKAb}2(+9i7c#81w8(y4TpXDil<$aNu3rHC?gj_q_lk zJ9s4gP=u9-AT|IWWNh;vuXVPvZSBuA%O1-Q?3jC4XRZhvlWRcVPSIRT#dlRjZ1r`V zerFD#Vpv(+WR7>IeqI~{XL+=;q~Tev$aNc6rkp9Md&Z;)C4m&F!~LY$Iwy_+uYI;X zj+$+@>)f?@qTm<+>gvEcf#V`u93$d0{(JEYMhY80%vFK8PGB z)s=Wt4(%Qe1?X-a|A_P&6UhT*$~xkA?g|^M#j=0|jy|&Se0G;)pwNBVE#~Aq*0c*1 zl$qo2T&74@&)pe;l0j3pS)1PaG65N77WQOi*XJkG1Y~qm^tKmtolWjDMlSnq8#U+{ zUX!|2YF(BuFP+rwqx(t#stU`A2_=IX=#2r{eaI<-q2%nZqhkeRL2A0u)cj9tUJz@Z zpG{2-pV2Pc_1Nx?n$z5zYN|P-LjVPO(_-v&z@HoH5zd9A@ARP}W^$TZrG7#%y17S+ z$*tX=9!r5=x|p+w5Fgqu%a-Ke`v4Dy0$(FV7p5ZA$B09Y)`F(4_|O;l<1H zhPD^B%LAx@tQ`bMLm+1IoH4o3%VUe@xzsHgNtbuLXu(xZLQ&^mH5;2BeVYRS)lw=c zUE0ehGx@xunn2bZvBM3~tAsj915E^(7TQ4e^J?3z^(SXBNA*ho((-ZS@pZ5i3}Kg zy4>(E?;S1ip|CIec4t1H)7SV<;1S-)pwEYQ+OKx8(BO{8bNHOb*r;B~U8vNQk_Rp; ze2~ps=F$u0jVWy3{Q{d(ASUYQruODnYwfsy=MhKw+?Z5{d!N#;sHXOAskd8ertX9()|6q*VpLp8Y1OBg3 ULG4sSNRI(D)$XDzZd-@`3q!G44FCWD literal 0 HcmV?d00001 diff --git a/docs/images/Hellion.png b/docs/images/Hellion.png new file mode 100644 index 0000000000000000000000000000000000000000..b9b56cde9596ae6555c051ac723d64e138c1e57b GIT binary patch literal 163968 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

    x^)b%mw` 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%0Dv#ad`iNRx!Yqo56 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 LNkvXXu0mjfa({cg diff --git a/docs/images/hellion-forge_logo_color_horizontal_pixel.png b/docs/images/hellion-forge_logo_color_horizontal_pixel.png new file mode 100644 index 0000000000000000000000000000000000000000..e50630c936036f930255fcdcb56549bcf40c42f0 GIT binary patch literal 40663 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 z

    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⪚lh7f+Hb*=iLh$t#4&p0n{=|URs*X1p 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 au}9av{tB literal 0 HcmV?d00001 diff --git a/docs/images/hellion-forge_logo_discord_hammer_512.png b/docs/images/hellion-forge_logo_discord_hammer_512.png new file mode 100644 index 0000000000000000000000000000000000000000..f569e271fc905f259d88a07f6a4dc76265c9adc5 GIT binary patch literal 7817 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 Z`UC&3Ss{I~NgOlaq=mhCovAnJ{{Zxk!x{hp literal 0 HcmV?d00001 -- 2.52.0 From f455bf4736f56ad22d64b5aacf7d650c9d12b1a9 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Fri, 8 May 2026 08:51:27 +0200 Subject: [PATCH 165/169] chore: drop stale Cycle reference from BrandingLinks comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The comment on BrandingLinks claimed a follow-up housekeeping sweep was "out of scope for this Cycle" — that Cycle framing no longer matches how Plan v4 schedules the work. Trim the trailing clause; the rest of the comment still documents the housekeeping intent. --- HellionChat/Branding/BrandingLinks.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/HellionChat/Branding/BrandingLinks.cs b/HellionChat/Branding/BrandingLinks.cs index 0298469..45adc57 100644 --- a/HellionChat/Branding/BrandingLinks.cs +++ b/HellionChat/Branding/BrandingLinks.cs @@ -4,8 +4,7 @@ namespace HellionChat.Branding; // Centralised so a future invite rotation only touches one file. The same // link is currently hard-coded in repo.json, README.md, SUPPORT.md, // CONTRIBUTORS.md and HellionChat.yaml — those will be migrated to consume -// this constant in a separate housekeeping sweep, but that's out of scope -// for this Cycle. +// this constant in a separate housekeeping sweep internal static class BrandingLinks { public const string HellionForgeDiscordInvite = "https://discord.gg/X9V7Kcv5gR"; -- 2.52.0 From 12ce015d830eb7e3d936728233c5fca73b44952c Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Fri, 8 May 2026 13:27:39 +0200 Subject: [PATCH 166/169] test: add TEST-MIRROR pointer to Build-Suite MigrationLogic --- HellionChat/Plugin.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index c8d4cd3..f7a4d42 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -349,6 +349,8 @@ public sealed class Plugin : IDalamudPlugin } } + // TEST-MIRROR: Hellion Build test/_Helpers/MigrationLogic.cs (local-only repo) + // If you change the formula here, change the mirror — tests assert the contract. if (oldWindowAlpha != 100f && Math.Abs(Config.WindowOpacity - 0.85f) < 0.001f) { -- 2.52.0 From 9640d336a6597019b8cba523c8325d547aa60fdf Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Fri, 8 May 2026 14:06:44 +0200 Subject: [PATCH 167/169] Migrate Actions workflows to Gitea MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - codeql.yml removed: GitHub-only (uses github/codeql-action/*). - build.yml + release.yml: runs-on switched to ubuntu-latest (Gitea Cloud has no Windows runner). Dalamud staging is now downloaded via curl/unzip into $HOME/.xlcore/dalamud/Hooks/dev/, the path the Dalamud SDK 15 uses on Linux. Locate-step uses find instead of Get-ChildItem. - release.yml: softprops/action-gh-release replaced with the Gitea-native https://gitea.com/actions/release-action. Auto-injected GITHUB_TOKEN on Gitea Actions has Gitea-API scope and is sufficient. - forge-announce.yml: environment: Webhook removed (Gitea has no environments — DISCORD_FORGE_WEBHOOK is a repo-level Actions secret). avatar_url and embed url switched from raw.githubusercontent.com / github.com to gitea.com. - release-footer.md: install URL plus the five doc links (README, PRIVACY, THIRD_PARTY_NOTICES, SECURITY, SUPPORT) and LICENSE link switched to gitea.com/.../src/branch/main/. ChatTwo upstream link stays on GitHub. --- .github/release-footer.md | 14 ++--- .github/workflows/build.yml | 17 +++-- .github/workflows/codeql.yml | 93 ---------------------------- .github/workflows/forge-announce.yml | 11 ++-- .github/workflows/release.yml | 48 +++++++------- 5 files changed, 50 insertions(+), 133 deletions(-) delete mode 100644 .github/workflows/codeql.yml diff --git a/.github/release-footer.md b/.github/release-footer.md index 1b98124..6ebc652 100644 --- a/.github/release-footer.md +++ b/.github/release-footer.md @@ -8,19 +8,19 @@ Dalamud main plugin repo. To install: 1. In XIVLauncher: **Settings → Experimental → Custom Plugin Repositories** 2. Add the URL: - `https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/repo.json` + `https://gitea.com/JonKazama-Hellion/HellionChat/raw/branch/main/repo.json` 3. Enable, save, then `/xlplugins` → search **Hellion Chat** → install ## Project documents -- [README](https://github.com/JonKazama-Hellion/HellionChat/blob/main/README.md) — features, architecture, build -- [Privacy notice](https://github.com/JonKazama-Hellion/HellionChat/blob/main/PRIVACY.md) — what the plugin stores and sends -- [Third-party notices](https://github.com/JonKazama-Hellion/HellionChat/blob/main/docs/THIRD_PARTY_NOTICES.md) — dependencies and licences -- [Security policy](https://github.com/JonKazama-Hellion/HellionChat/blob/main/SECURITY.md) — vulnerability reporting -- [Support](https://github.com/JonKazama-Hellion/HellionChat/blob/main/SUPPORT.md) — bug reports, questions, contact paths +- [README](https://gitea.com/JonKazama-Hellion/HellionChat/src/branch/main/README.md) — features, architecture, build +- [Privacy notice](https://gitea.com/JonKazama-Hellion/HellionChat/src/branch/main/PRIVACY.md) — what the plugin stores and sends +- [Third-party notices](https://gitea.com/JonKazama-Hellion/HellionChat/src/branch/main/docs/THIRD_PARTY_NOTICES.md) — dependencies and licences +- [Security policy](https://gitea.com/JonKazama-Hellion/HellionChat/src/branch/main/SECURITY.md) — vulnerability reporting +- [Support](https://gitea.com/JonKazama-Hellion/HellionChat/src/branch/main/SUPPORT.md) — bug reports, questions, contact paths ## Licence -[EUPL-1.2](https://github.com/JonKazama-Hellion/HellionChat/blob/main/LICENSE). +[EUPL-1.2](https://gitea.com/JonKazama-Hellion/HellionChat/src/branch/main/LICENSE). Based on [Chat 2](https://github.com/Infiziert90/ChatTwo) by Infi and Anna, also EUPL-1.2. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b2e4663..a03b780 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,6 +3,12 @@ name: Build # Verifies that every push to main and every PR still builds against the # current Dalamud staging branch. Does not produce release artefacts; the # release workflow handles that on tag. +# +# Linux runner: gitea.com Cloud Actions provides ubuntu-latest. The plugin +# csproj targets net10.0-windows, but `dotnet build` cross-compiles on +# Linux as long as the Dalamud staging assemblies are present at the +# expected lookup path ($(HOME)/.xlcore/dalamud/Hooks/dev/, which the +# Dalamud SDK 15 uses on Linux). on: push: @@ -21,7 +27,7 @@ permissions: jobs: build: name: Build (Release) - runs-on: windows-latest + runs-on: ubuntu-latest timeout-minutes: 15 steps: @@ -34,12 +40,11 @@ jobs: dotnet-version: 10.0.x - name: Download Dalamud staging - shell: pwsh run: | - $hooks = Join-Path $env:APPDATA "XIVLauncher\addon\Hooks\dev" - New-Item -ItemType Directory -Force -Path $hooks | Out-Null - Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile dalamud.zip - Expand-Archive -Force -Path dalamud.zip -DestinationPath $hooks + hooks="$HOME/.xlcore/dalamud/Hooks/dev" + mkdir -p "$hooks" + curl -fsSL https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -o dalamud.zip + unzip -oq dalamud.zip -d "$hooks" - name: Restore run: dotnet restore HellionChat/HellionChat.csproj diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 2b78608..0000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,93 +0,0 @@ -name: CodeQL - -# Replaces the GitHub default-setup CodeQL scan. The default setup runs -# without resolving the Dalamud assemblies (they live in a user-AppData -# path) and reports "Low C# analysis quality" because call-target -# resolution sits at ~64%. This workflow downloads the Dalamud staging -# distribution before the build, runs a manual dotnet build, and then -# lets CodeQL analyse the fully-resolved compilation. Quality climbs -# back above the 85% thresholds. -# -# This workflow only consumes trusted inputs: the tag/branch ref via -# the standard checkout action, and the Dalamud distribution URL which -# is pinned to a goatcorp-controlled GitHub Pages target. No user- -# controlled event payload (issue title, PR body, commit message) flows -# into a run-step. -# -# Disable the default setup in the repo before this workflow lands: -# Settings -> Code security -> Code scanning -> "CodeQL analysis" tile -# -> Switch to advanced. - -on: - push: - branches: [main] - pull_request: - branches: [main] - schedule: - - cron: '17 6 * * 1' - -permissions: - actions: read - contents: read - security-events: write - -jobs: - analyze-csharp: - name: Analyze (csharp) - runs-on: windows-latest - timeout-minutes: 30 - - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Setup .NET 10 - uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 - with: - dotnet-version: 10.0.x - - - name: Download Dalamud staging - shell: pwsh - run: | - $hooks = Join-Path $env:APPDATA "XIVLauncher\addon\Hooks\dev" - New-Item -ItemType Directory -Force -Path $hooks | Out-Null - Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile dalamud.zip - Expand-Archive -Force -Path dalamud.zip -DestinationPath $hooks - - - name: Initialize CodeQL - uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 - with: - languages: csharp - build-mode: manual - queries: security-extended - - - name: Restore - run: dotnet restore HellionChat/HellionChat.csproj - - - name: Build (Release) - run: dotnet build HellionChat/HellionChat.csproj --configuration Release --no-restore - - - name: Perform CodeQL analysis - uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 - with: - category: /language:csharp - - analyze-actions: - name: Analyze (actions) - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Initialize CodeQL - uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 - with: - languages: actions - build-mode: none - - - name: Perform CodeQL analysis - uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 - with: - category: /language:actions diff --git a/.github/workflows/forge-announce.yml b/.github/workflows/forge-announce.yml index 86b8fb8..69f5257 100644 --- a/.github/workflows/forge-announce.yml +++ b/.github/workflows/forge-announce.yml @@ -34,10 +34,9 @@ jobs: announce: name: Post changelog to Hellion Forge runs-on: ubuntu-latest - # The DISCORD_FORGE_WEBHOOK secret lives under Settings → Environments - # → Webhook (case-sensitive). Without this declaration the secret is - # not in scope for the job. - environment: Webhook + # The DISCORD_FORGE_WEBHOOK secret is set as a repo-level Actions Secret + # on Gitea (Settings → Actions → Secrets). Repo-level secrets are in + # scope for every job by default, no environment: declaration needed. timeout-minutes: 5 steps: @@ -134,7 +133,7 @@ jobs: # ---------- Embed-Payload bauen ---------- $payload = [ordered]@{ username = "Forge Herald" - avatar_url = "https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/icon.png" + avatar_url = "https://gitea.com/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png" content = "<@&1500489631555260446>" allowed_mentions = [ordered]@{ parse = @() @@ -143,7 +142,7 @@ jobs: embeds = @( [ordered]@{ title = $title - url = "https://github.com/JonKazama-Hellion/HellionChat/releases/tag/$tag" + url = "https://gitea.com/JonKazama-Hellion/HellionChat/releases/tag/$tag" color = 12730636 description = $description footer = [ordered]@{ text = $footerText } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6283609..edd8855 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,15 +2,19 @@ name: Release # Triggered when a vX.Y.Z tag is pushed. Builds the plugin against the # current Dalamud staging branch, locates the latest.zip produced by -# DalamudPackager and attaches it to the matching GitHub Release. +# DalamudPackager and attaches it to the matching Gitea Release. # # User-controlled inputs touched by this workflow: # - the tag name (filtered by on.tags = v*, validated again at runtime # against ^v\d+\.\d+\.\d+$ before being used in any string) # All other values are either repo-controlled (paths under -# HellionChat/bin/Release derived from Get-ChildItem) or pinned URLs to -# goatcorp / GitHub. Nothing from a webhook event payload (issue/PR +# HellionChat/bin/Release derived from find / Get-ChildItem) or pinned +# URLs to goatcorp / gitea. Nothing from a webhook event payload (issue/PR # titles, commit messages, etc.) flows into a run-step. +# +# Linux runner: gitea.com Cloud Actions only ships ubuntu-latest. The +# plugin csproj targets net10.0-windows, `dotnet build` cross-compiles on +# Linux when the Dalamud staging assemblies sit under $(HOME)/.xlcore/... on: push: @@ -33,7 +37,7 @@ permissions: jobs: release: name: Build and attach release ZIP - runs-on: windows-latest + runs-on: ubuntu-latest timeout-minutes: 20 steps: @@ -52,27 +56,25 @@ jobs: dotnet-version: 10.0.x - name: Download Dalamud staging - shell: pwsh run: | - $hooks = Join-Path $env:APPDATA "XIVLauncher\addon\Hooks\dev" - New-Item -ItemType Directory -Force -Path $hooks | Out-Null - Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile dalamud.zip - Expand-Archive -Force -Path dalamud.zip -DestinationPath $hooks + hooks="$HOME/.xlcore/dalamud/Hooks/dev" + mkdir -p "$hooks" + curl -fsSL https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -o dalamud.zip + unzip -oq dalamud.zip -d "$hooks" - name: Build (Release) run: dotnet build HellionChat/HellionChat.csproj --configuration Release - name: Locate latest.zip id: locate - shell: pwsh run: | - $zip = Get-ChildItem -Path HellionChat\bin\Release -Recurse -Filter latest.zip | Select-Object -First 1 - if (-not $zip) - { - throw "latest.zip not found under HellionChat\bin\Release" - } - Write-Host "Found: $($zip.FullName)" - "path=$($zip.FullName)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + zip="$(find HellionChat/bin/Release -name latest.zip -print -quit)" + if [ -z "$zip" ]; then + echo "latest.zip not found under HellionChat/bin/Release" >&2 + exit 1 + fi + echo "Found: $zip" + echo "path=$zip" >> "$GITHUB_OUTPUT" # Build a release body from the matching changelog block in # HellionChat.yaml plus a static install / docs footer. Fails the @@ -150,8 +152,13 @@ jobs: Write-Host $body Write-Host "----------------------------------------" - - name: Attach to GitHub release - uses: softprops/action-gh-release@v3 + # Gitea-native release action. Creates the release if the tag has no + # release yet, or updates the existing one. body_path provides the + # generated release body, files attaches latest.zip. The auto-injected + # GITHUB_TOKEN on Gitea Actions has Gitea-API scope and is sufficient + # for release write. + - name: Attach to Gitea release + uses: https://gitea.com/actions/release-action@main with: # Explicit tag_name so the action targets the correct release in # both push:tags (auto) and workflow_dispatch (manual recovery) @@ -160,5 +167,4 @@ jobs: tag_name: ${{ github.event.inputs.tag || github.ref_name }} files: ${{ steps.locate.outputs.path }} body_path: release-body.md - fail_on_unmatched_files: true - generate_release_notes: false + api_key: ${{ secrets.GITHUB_TOKEN }} -- 2.52.0 From 9a8a01479598d2e83a69ce3e15b91e10389e18e0 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Fri, 8 May 2026 15:00:30 +0200 Subject: [PATCH 168/169] docs: close active upstream cherry-pick pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chat 2 has entered a major rework that Infi confirmed makes selective patches no longer portable. The cherry-pick pipeline as a routine workflow stops with the v1.4.x cycle. Documentation reflects the new state across all touchpoints. UPSTREAM_SYNC.md rewritten: replaces the "How I Cherry-Pick" / "Reviewing What Is New Upstream" / "Conflict Handling" sections with "Why Cherry-Picking Stopped", "What Closing the Pipeline Means in Practice", "What Does Not Change", "What Could Re-Open Later". Existing cherry-pick trails in the git history stay intact, EUPL-1.2 anchor lines and NOTICE.md remain canonical. README.md, CONTRIBUTING.md, ROADMAP.md, THIRD_PARTY_NOTICES.md and the PR template updated to match: cherry-pick references reframed as historical or pointed at UPSTREAM_SYNC.md for the current state. NOTICE.md keeps the BetterTTV cherry-pick example as a concrete past case but adds a paragraph that the pipeline is closed and clarifies the attribution standard is preserved unchanged. PULL_REQUEST_TEMPLATE.md drops the "Upstream cherry-pick from Chat 2" checkbox and the cherry-pick-path compatibility prompt. The upstream git remote was already removed locally on 2026-05-08 (separate change, not in this commit). No source-file edits, no manifest version bump, no changelog entry — this is documentation-only and ships with the next release. --- .github/PULL_REQUEST_TEMPLATE.md | 2 - CONTRIBUTING.md | 28 ++++-- NOTICE.md | 27 +++-- README.md | 4 +- docs/ROADMAP.md | 7 +- docs/THIRD_PARTY_NOTICES.md | 5 +- docs/UPSTREAM_SYNC.md | 165 +++++++++++++------------------ 7 files changed, 117 insertions(+), 121 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d4542c0..9245be4 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -23,7 +23,6 @@ https://github.com/JonKazama-Hellion/HellionChat/security/advisories/new - [ ] Documentation only - [ ] Translation update - [ ] Build, CI or tooling change -- [ ] Upstream cherry-pick from Chat 2 ## Linked issue @@ -53,7 +52,6 @@ new commands, new translations, removed behaviour. If none, write bump and is it covered by the existing migration tests? - Does this change the schema in MessageStore? - Does this change the repo.json or HellionChat.yaml manifest fields? -- Does this affect the upstream cherry-pick path? See docs/UPSTREAM_SYNC.md. --> ## Checklist diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7de7a9e..1ba0500 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,9 +15,11 @@ to make a contribution land smoothly. - Read the [README](README.md) so you understand the scope: a privacy-focused, EUPL-1.2-licensed Dalamud plugin that intentionally removes the upstream webinterface and ships privacy-first defaults. -- Read [`docs/UPSTREAM_SYNC.md`](docs/UPSTREAM_SYNC.md). Cherry-picks - from upstream Chat 2 are selective and deliberate; not everything - that lands there belongs here. +- Read [`docs/UPSTREAM_SYNC.md`](docs/UPSTREAM_SYNC.md). Active + cherry-picking from upstream Chat 2 has ended in the v1.4.x cycle; + HellionChat continues as an independent codebase. Existing + upstream-derived code keeps its attribution. New contributions + stand on their own and do not need to be cherry-pick-compatible. - Read [`SECURITY.md`](SECURITY.md). Anything security-sensitive goes through a private advisory, never a public issue or PR. - Read the [Code of Conduct](CODE_OF_CONDUCT.md). @@ -43,9 +45,11 @@ to make a contribution land smoothly. "Was gegenüber Chat 2 fehlt". - Features that bypass the privacy filter or weaken the default retention behaviour without an explicit, documented opt-in. -- Sweeping refactors that touch large parts of the codebase. They make - selective upstream cherry-picks much harder and the maintenance cost - outweighs the benefit for a one-person project. +- Sweeping refactors that touch large parts of the codebase. The + maintenance cost outweighs the benefit for a one-person project. + (This used to be doubly important because of the upstream + cherry-pick path; that path is closed now, but the rule still + holds on its own merits.) - AI-generated code dropped in without disclosure or human review. See [`docs/AI_DISCLOSURE.md`](docs/AI_DISCLOSURE.md) for how I handle AI assistance on my side; I expect comparable transparency from @@ -117,9 +121,15 @@ Hellion-specific strings live in direct pull requests. The upstream Chat 2 strings in `HellionChat/Resources/Language.*.resx` -are **not** translated here. They are owned by the upstream project -and synced in via cherry-pick. Please contribute those to -[Infiziert90/ChatTwo](https://github.com/Infiziert90/ChatTwo) instead. +are **not** translated here. They are kept as-is from the last +upstream sync and remain the work of the Chat 2 Crowdin community. +Active cherry-picking from upstream ended in the v1.4.x cycle (see +[`docs/UPSTREAM_SYNC.md`](docs/UPSTREAM_SYNC.md)), so future +translation improvements to those upstream strings will not flow +into HellionChat automatically anymore. If you have improvements +for the original Chat 2 strings, please contribute them to +[Infiziert90/ChatTwo](https://github.com/Infiziert90/ChatTwo) +directly. ## Licensing diff --git a/NOTICE.md b/NOTICE.md index 786f887..9043229 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -35,9 +35,19 @@ edits minimal, isolated to clearly-marked Hellion files, and reversible. Concrete example: when API 15 hit, I cherry-picked your fix for the BetterTTV emote regression with `git cherry-pick -x` so authorship and -co-author trail stay intact. That is the standard I want to keep using as -long as both projects are alive. You should never have to look at this -fork and wonder if I quietly ate your work. +co-author trail stay intact. That was the standard I held to as long +as cherry-picking was viable, and you should never have to look at +this fork and wonder if I quietly ate your work. + +With ChatTwo entering its rework cycle, the active cherry-pick +pipeline is closed since v1.4.x — see [docs/UPSTREAM_SYNC.md](docs/UPSTREAM_SYNC.md) +for the full reasoning. The attribution standard stays exactly the +same: every existing `(cherry picked from commit ...)` line remains +in the git history, the EUPL-1.2 anchor lines in source files are +untouched, and this NOTICE.md remains canonical. If anything from +this point forward originates from Chat 2 it will be a hand-port at +most, called out as such in the commit message and source comments, +not a `git cherry-pick`. If anything in this fork ever steps on something you would not be okay with, please reach out and I will fix it. Genuinely. The list of contacts @@ -62,8 +72,10 @@ full-history-by-default position fits a much larger one, including the roleplaying community where chat archive is part of the play experience. Trying to upstream HellionChat's defaults would have meant arguing that Chat 2's defaults are wrong, and they are not. They are right for the -user base ChatTwo serves. So I keep the fork separate, attribute clearly, -and pull selected upstream patches when they apply. +user base ChatTwo serves. So I keep the fork separate and attribute +clearly. Active cherry-picking from upstream stopped in the v1.4.x +cycle once Chat 2's rework made selective patches no longer portable; +the existing cherry-pick trail stays in the git history. ## Why HellionChat left the GitHub fork network @@ -72,8 +84,9 @@ that a fork is either a development branch or a dead mirror. HellionChat is neither. It is an independently-maintained EUPL-1.2 fork with its own release cadence, its own custom repo, its own user base. Detaching the fork-network relation just makes the situation honest. The git history, -the cherry-pick trail, and the attribution stay exactly the same. The -only thing that changes is the GitHub UI no longer says "forked from". +the existing cherry-pick trail, and the attribution stay exactly the +same. The only thing that changes is the GitHub UI no longer says +"forked from". ## Trademarks and naming diff --git a/README.md b/README.md index 992f6e3..b01f483 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Hellion Chat ist ein Privacy-First-Plugin auf dem Chat-2-Fundament. Der größte Der Daten-Handling-Fokus liegt auf den DSGVO/EU-, US- und JP-Regelungen, soweit für ein Chat-Plugin praktisch umsetzbar: Speicherzeit pro Kanal, granulare Filter, Selbstauskunft per Export. Eine ausführliche Auflistung steht in [`PRIVACY.md`](PRIVACY.md). -Eigenständiges Repository, EUPL-1.2-lizenziert. Mit v1.0.0 ist der Standalone-Cut abgeschlossen: eigener Namespace `HellionChat.*`, eigene IPC-Kanäle, eigene Source-Tree-Struktur. Distribution über Custom-Repo. Selektive Cherry-Picks von Upstream-Chat-2 nach Bedarf, dokumentiert in [`docs/UPSTREAM_SYNC.md`](docs/UPSTREAM_SYNC.md). +Eigenständiges Repository, EUPL-1.2-lizenziert. Mit v1.0.0 ist der Standalone-Cut abgeschlossen: eigener Namespace `HellionChat.*`, eigene IPC-Kanäle, eigene Source-Tree-Struktur. Distribution über Custom-Repo. Aktiver Upstream-Sync ist mit dem v1.4.x-Cycle beendet: Chat 2 befindet sich in einem grundlegenden Rework und Cherry-Picks sind nicht mehr portierbar. Hellion Chat geht ab da als unabhängige Codebase weiter, Hintergrund und Attribution in [`docs/UPSTREAM_SYNC.md`](docs/UPSTREAM_SYNC.md). ## Acknowledgements @@ -311,7 +311,7 @@ Im Repo-Root liegen die Standard-Repository-Dokumente, vertiefende Dokumentation | [`docs/LEARNING-JOURNEY.md`](docs/LEARNING-JOURNEY.md) | Entwicklungsgeschichte, vom Web-Stack zu C# / Dalamud, was ich aus dem Fork gelernt habe. | | [`docs/IPC.md`](docs/IPC.md) | IPC-Kanal-Reference, Tuple-Payload-Felder, Migrations-Diff für Drittplugins. | | [`docs/THEME-AUTHORING.md`](docs/THEME-AUTHORING.md) | Theme-Engine-Authoring-Guide (EN): JSON-Schema, Color- und Layout-Slots, Channel-Identity-Regeln, Validierung. | -| [`docs/UPSTREAM_SYNC.md`](docs/UPSTREAM_SYNC.md) | Cherry-Pick-Policy gegenüber Chat 2. | +| [`docs/UPSTREAM_SYNC.md`](docs/UPSTREAM_SYNC.md) | Upstream-Sync-Stand: Cherry-Pick-Pipeline seit v1.4.x geschlossen, Attribution intakt. | | [`docs/THIRD_PARTY_NOTICES.md`](docs/THIRD_PARTY_NOTICES.md) | NuGet-Dependencies mit Lizenzen, Bundled Assets, Network-Status pro Komponente. | | [`docs/AI_DISCLOSURE.md`](docs/AI_DISCLOSURE.md) | Offenlegung der KI-Unterstützung im Entwicklungsprozess. | diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index a613750..9abaa0d 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -203,6 +203,7 @@ aktuellen Stand getestet. Hellion Chat ist EUPL-1.2-lizenziert. Konzept-Imports aus AGPL-3.0-Plugins (z.B. XIV Instant Messenger) sind ausschließlich architektonische -Inspiration, kein Code-Port. Imports aus dem GPL-3.0-kompatiblen -Upstream-Bestand laufen weiter über -[`UPSTREAM_SYNC.md`](UPSTREAM_SYNC.md). +Inspiration, kein Code-Port. Code-Imports aus dem Upstream-Bestand +sind seit v1.4.x abgeschlossen, weil Chat 2 in einem grundlegenden +Rework ist und selektive Patches nicht mehr sauber portierbar sind. +Stand und Begründung in [`UPSTREAM_SYNC.md`](UPSTREAM_SYNC.md). diff --git a/docs/THIRD_PARTY_NOTICES.md b/docs/THIRD_PARTY_NOTICES.md index 5a79c65..5255fbc 100644 --- a/docs/THIRD_PARTY_NOTICES.md +++ b/docs/THIRD_PARTY_NOTICES.md @@ -51,8 +51,9 @@ HellionChat is a fork of [Chat 2](https://github.com/Infiziert90/ChatTwo) by Infiziert90 (Infi) and Anna Clemens, also licensed under EUPL-1.2. The bulk of the code, including the message store architecture, the channel logic, the hook system and the ImGui chat window, originates -from upstream. See `../NOTICE.md` and `UPSTREAM_SYNC.md` for the -attribution and the cherry-pick policy. +from upstream. See `../NOTICE.md` for the attribution; `UPSTREAM_SYNC.md` +documents the upstream-sync history, including the close of active +cherry-picking in the v1.4.x cycle. --- diff --git a/docs/UPSTREAM_SYNC.md b/docs/UPSTREAM_SYNC.md index 8bbd786..b5be49a 100644 --- a/docs/UPSTREAM_SYNC.md +++ b/docs/UPSTREAM_SYNC.md @@ -2,12 +2,12 @@ HellionChat is a standalone EUPL-1.2 plugin that originated from [Chat 2](https://github.com/Infiziert90/ChatTwo). Since v1.0.0 it -lives under its own namespace, IPC channels and source tree. I no -longer track upstream as a Git fork, but I do monitor Chat 2 commits -regularly and cherry-pick selectively where it makes sense. +lives under its own namespace, IPC channels and source tree. The +active cherry-pick pipeline from upstream Chat 2 is closed since +the v1.4.x cycle. -This document covers how that works so anyone (including future-me) -can do it cleanly. +This document covers what that means, why I closed it, and what +stays in place. ## A Word on Intent @@ -28,99 +28,77 @@ new UI from scratch and making deliberate architectural decisions that pull in a different direction. Some upstream patches will simply stop applying cleanly and that is expected. -## One-Time Setup +## Why Cherry-Picking Stopped in v1.4.x -Add the upstream repo as a remote on a fresh clone: +Two things converged: -```bash -git remote add upstream https://github.com/Infiziert90/ChatTwo.git -git fetch upstream -``` +1. **Chat 2 is in a rework cycle.** Infi mentioned directly that + parts of ChatTwo are being reworked and "stuff may not be able to + be cherry picked anymore." Once the upstream code paths I would + pull from no longer exist in the same shape, `git cherry-pick` + stops being a meaningful tool — what would land would not be the + change Infi wrote, it would be a hand-port of his concept. +2. **HellionChat has drifted enough that selective patches require + adaptation anyway.** The UI is being rebuilt, the theme engine + sits on top of HellionStyle which has no upstream equivalent, the + privacy filter changes how messages flow through MessageManager. + Even before the rework was announced, more and more upstream + patches needed adaptation rather than a clean apply. -Verify both remotes are wired up: +Together those two points mean continuing to call this an "active +cherry-pick pipeline" was no longer honest. So I closed it. -```bash -git remote -v -# origin https://github.com/JonKazama-Hellion/HellionChat.git (fetch) -# origin https://github.com/JonKazama-Hellion/HellionChat.git (push) -# upstream https://github.com/Infiziert90/ChatTwo.git (fetch) -# upstream https://github.com/Infiziert90/ChatTwo.git (push) -``` +## What Closing the Pipeline Means in Practice -`upstream` is read-only. Never push to it. +- The `upstream` git remote was removed locally on 2026-05-08. + Anyone setting up a fresh clone does **not** add it back. +- New commits will not carry `(cherry picked from commit ...)` + trailers. Anything that originates from Chat 2 from this point + forward will be a hand-port at most, and it gets called out as + such in its own commit message and in the relevant source comments. +- The existing cherry-pick trail stays in the git history exactly as + it is. Every `(cherry picked from commit ...)` line that was added + with `-x` in earlier releases remains intact; that is the + attribution paper trail and removing it would be wrong. -## Reviewing What Is New Upstream +## What Does Not Change -Before any feature cycle I run a quick check: +- **EUPL-1.2 anchor lines in source files.** Files that originated + from Chat 2 keep their licence headers and any "based on + Infiziert90/ChatTwo" notice exactly as they are. The licence + obligations under EUPL-1.2 do not lapse because cherry-picking + stopped. +- **NOTICE.md** stays canonical. Attribution to Infi and Anna for the + message store, channel logic, hook system, ImGui chat window and + the localisation infrastructure remains the foundation statement of + this fork. +- **README acknowledgements.** The Acknowledgements section in + `README.md`, the maintainer thanks in the About tab, and the + `Language.*.resx` Crowdin translator credit list all stay as they + are. +- **The original `Language.*.resx` files** remain in the source tree + in their last upstream-sync state. They are the work of the Chat 2 + Crowdin community and the existing translations stay valuable. They + will not receive automatic upstream updates anymore — see + CONTRIBUTING.md for what that means for translators. -```bash -git fetch upstream -git log --oneline main..upstream/main | head -30 -``` +## What Could Re-Open Later -That shows every commit Infi or contributors landed since the last -sync. I read the messages and decide which ones apply to HellionChat. +If Chat 2's rework lands and stabilises, and there is a piece of +upstream code that I genuinely want in HellionChat, the path forward +is **study and re-implement**, not cherry-pick. That means: -## What I Cherry-Pick +- Read the upstream change, understand the design, port the concept + to HellionChat's actual code paths. +- Credit the upstream author in the commit message and, if the + ported code is non-trivial, in a source-file comment. +- Pre-clear with Infi if the port is large enough to warrant a + conversation. -**Always:** security fixes, Dalamud API compatibility patches, -BetterTTV and emote-cache fixes, regression fixes for upstream -behaviour HellionChat still relies on. - -**Sometimes:** small bug fixes in `MessageManager.cs`, -`MessageStore.cs`, `ChatLogWindow.cs`, the Tabs system. These come in -when they touch code I have not heavily modified. - -**Never:** webinterface changes (the entire webinterface tree is gone -in HellionChat), changes that conflict with the privacy filter, changes -that re-add upstream defaults I deliberately reversed (full-history -logging, Tell Exclusive defaults, etc.). - -As HellionChat's UI moves further from the Chat 2 baseline, upstream -patches will increasingly require adaptation rather than a clean -apply. If a patch cannot be ported without breaking HellionChat -behaviour or the privacy model, I skip it rather than force a -compromised version in. - -## How I Cherry-Pick - -Always with `-x` so authorship and the original commit hash stay -visible: - -```bash -git checkout -b sync/upstream- main -git cherry-pick -x -``` - -`-x` appends a `(cherry picked from commit )` line to the commit -message. That preserves upstream-author credit and lets anyone reading -`git log` trace the change back to Chat 2. Commit messages stay -identical to the upstream original; I do not rewrite them to match the -HellionChat format. - -## Conflict Handling - -When a cherry-pick conflicts: - -1. Resolve by hand. Do not rewrite upstream code to match HellionChat - conventions; that is what the merge marker showed. -2. If the conflict is fundamental (touches code that no longer exists - in HellionChat), abort the cherry-pick and note why in the - relevant GitHub issue or backlog item. Some upstream patches are - simply not portable and that is fine. -3. After a clean resolve the commit message stays as-is, with the - `-x` footer Git appends automatically. - -## Pushing the Sync - -Cherry-picked commits go through the same review as any other change. -The sync branch lands in `main` via a no-fast-forward merge, then gets -a release tag if user-visible behaviour changed: - -```bash -git checkout main -git merge --no-ff sync/upstream- -m "merge: upstream sync — " -``` +This is heavier than `git cherry-pick -x` and that is the point. +Cherry-picking was light because both codebases shared structure; +once they do not, the proper attribution costs a real conversation +rather than a flag on a git command. ## Contributing Back @@ -138,17 +116,12 @@ A few things to note about that process: not push that decision onto his codebase. - This is not guaranteed for every change, only where it makes sense and where I am confident the fix is clean and self-contained. - -## When Upstream Goes Silent - -If Chat 2 stops receiving updates the remote stays configured and this -workflow stays documented. The moment maintenance picks back up I am -ready to pull again. +- Whether it gets accepted is Infi's call, and a "no" is fine. ## When Upstream Takes a Direction I Cannot Follow If a future Chat 2 release breaks compatibility with the HellionChat privacy philosophy in a way that cannot be resolved (mandatory cloud -sync, removal of the local message store, an incompatible license -change), HellionChat continues from the last compatible cherry-pick. -The inherited history stays under EUPL-1.2 and stays attributed. +sync, removal of the local message store, an incompatible licence +change), HellionChat continues from where it is. The inherited +history stays under EUPL-1.2 and stays attributed. -- 2.52.0 From 4c8b0da3da219a2ad8e1e1fd0cdee6a3dd55cc6f Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Fri, 8 May 2026 15:11:46 +0200 Subject: [PATCH 169/169] ci: drop upload-artifact step from build.yml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit actions/upload-artifact@v7 fails on Gitea Actions — the GitHub artifact API has compatibility gaps the Gitea runtime layer does not fully cover, and v7 specifically tripped exitcode 1 on the Strato runner. The build itself runs fine; the artefact was never consumed by anything (release.yml does its own latest.zip lookup), so the cleanest fix is to make build.yml a pure compile-health check without artefact upload. --- .github/workflows/build.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a03b780..d29a76e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -51,11 +51,3 @@ jobs: - name: Build (Release) run: dotnet build HellionChat/HellionChat.csproj --configuration Release --no-restore - - - name: Upload build output - uses: actions/upload-artifact@v7 - with: - name: HellionChat-build-${{ github.run_number }} - path: HellionChat/bin/Release/**/HellionChat/** - if-no-files-found: warn - retention-days: 14 -- 2.52.0