diff --git a/.github/forge-posts/v2.0.0.md b/.github/forge-posts/v2.0.0.md new file mode 100644 index 0000000..0a9122f --- /dev/null +++ b/.github/forge-posts/v2.0.0.md @@ -0,0 +1,11 @@ +--- +subtitle: "Rebuilt, Repaired, Reset" +versionsnatur: "Major-Release mit Config-Reset" +--- +- **Deine Einstellungen werden zurückgesetzt.** Neun Zyklen Umbau haben gespeicherte Werte hinterlassen, die auf Oberflächen zeigen, die es nicht mehr gibt. Neu anzufangen ist der einzige Weg, dass alle dieselben Vorgaben haben. **Dein Nachrichtenverlauf bleibt unberührt**, der liegt in einer eigenen Datenbank. Die alten Einstellungen liegen als `HellionChat.json.pre-2.0.0.bak` daneben. +- **Behoben, und mehrere davon haben Daten verloren oder versteckt:** Die rückwirkende Bereinigung ließ sich nie anwenden. Das Verdichten der Datenbank schlug fehl und meldete danach, es sei nichts gelöscht worden, während alles weg war. Gelöschte Nachrichten blieben im Suchindex. Angepinnte Flüster-Tabs kamen eine ganze Sitzung lang leer hoch. Der Export schrieb ungültiges JSON. Ein Auskoppelfenster mit Titelleiste ließ sich nicht schließen. Ein 403 eines Emote-Dienstes riss alle 65 funktionierenden Emotes mit. +- **Was sich am Speichern ändert:** Das Kanalraster entscheidet jetzt allein. Bisher griff die Unbekannt-Absicherung auch bei bekannten Kanälen, ein abgewählter Kanal wurde also trotzdem geschrieben. Wen das betraf, der speichert ab jetzt weniger. Nichts in der Datenbank wird angefasst. +- **Jedes Fenster zeichnet das Plugin selbst** und alle sprechen eine Sprache: Struktur trägt die Typografie, alles Drückbare bekommt eine Fläche, jede Farbe wird gegen ihren Untergrund gemessen. Dazu benannte Typo-Rollen, Zeitstempel in eigener Spalte, kursive Systemmeldungen. +- **Wieder erreichbar:** Export, Tab-Editor, Datenbankpflege und Anpinnen hatten beim Umbau ihre Zugänge verloren. Der Screenshot-Modus erreichte eine von vier Flächen mit Tab-Namen, jetzt alle vier. Der DSGVO-Hinweis beim Profil "Volle Historie" war in 25 Sprachen übersetzt und seit Mai unsichtbar. +- **Gestrichen:** sechs Einstellungen mit Regler, gespeichertem Wert und ohne jeden Leser. Ein Regex-Filter pro Tab, den der spieleigene Wortfilter abdeckt. Ein Assistenten-Häkchen, das abgefragt, als angewendet gemeldet und nie gelesen wurde. +- **Neu:** Emote-Tab im Standard-Layout (Testerwunsch), Orts- und Serverzeit in der Statusleiste, Screenshot-Modus aus der Eingabezeile erreichbar, `/hellion wizard`, ein Stil-Labor unter `/hellion lab`, und ein Hinweis im Assistenten, dass Plugins in FFXIV eine Grauzone sind und nicht in öffentliche Kanäle gehören. diff --git a/HellionChat/AutoTellTabsService.cs b/HellionChat/AutoTellTabsService.cs index 66f4838..ac01d39 100644 --- a/HellionChat/AutoTellTabsService.cs +++ b/HellionChat/AutoTellTabsService.cs @@ -23,7 +23,7 @@ internal sealed class AutoTellTabsService : IDisposable private readonly ILogger _logger; // Tabs-list structure lock now lives on Plugin (neutral owner) so the - // MessageManager refilter can share it. See Plugin.TabsListLock / B3. + // MessageManager refilter can share it. See Plugin.TabsListLock. private object TabsListLock => _plugin.TabsListLock; // Bumped whenever something wipes unpinned temp tabs wholesale (logout). @@ -58,8 +58,8 @@ internal sealed class AutoTellTabsService : IDisposable // Derived from the tab list on read. Pin/Unpin/Promote/Logout simply // mutate IsPinned or remove tabs — the count adapts automatically. - // Replaces the F2.1 Interlocked counter because the new pin-state - // transitions are cold-path and don't need lock-free reads. + // Replaces an Interlocked counter: the pin-state transitions are cold-path + // and don't need lock-free reads. internal int ActiveTempTabCount => Plugin.Config.Tabs.Count(TabLifecycleHelpers.IsInUnpinnedPool); @@ -179,7 +179,7 @@ internal sealed class AutoTellTabsService : IDisposable } // Three steps, because building the tab pulls history out of the store and - // that must not happen under TabsListLock (B3 rule; the query sorts the whole + // that must not happen under TabsListLock (the query sorts the whole // receiver history). Step 1 and 3 are locked, step 2 is not. int generation; lock (TabsListLock) @@ -303,7 +303,7 @@ internal sealed class AutoTellTabsService : IDisposable internal void DropOldestTempTab() { - // B3: lock the list-structure ops so the (currently caller-less) Unpin path + // Lock the list-structure ops so the (currently caller-less) Unpin path // can't race the worker; re-entrant when HandleTell already holds the lock. lock (TabsListLock) { @@ -577,7 +577,7 @@ internal sealed class AutoTellTabsService : IDisposable // Count and flag under one lock so the cap can't be raced. SaveConfig stays // OUTSIDE -- holding TabsListLock across a save would put an fsync on the - // click path, which is what B6 just removed elsewhere. + // click path, which is what a later cycle just removed elsewhere. lock (TabsListLock) { if (PinnedTempTabCount >= MaxPinnedTempTabs) diff --git a/HellionChat/CjkFallbackRange.cs b/HellionChat/CjkFallbackRange.cs index d7d16db..d18210c 100644 --- a/HellionChat/CjkFallbackRange.cs +++ b/HellionChat/CjkFallbackRange.cs @@ -1,7 +1,7 @@ namespace HellionChat; -// Reduced CJK fallback coverage for the v1.5.3 NotoSansCjk fallback merge (B1). -// Before B1 the fallback merged over the full `Ranges` array (Default + endonyms), +// Reduced CJK fallback coverage for the v1.5.3 NotoSansCjk fallback merge. +// Before that the fallback merged over the full `Ranges` array (Default + endonyms), // duplicating the Latin/Default work already done by the global/Japanese fonts. // This is the trimmed remainder the fallback is actually the sole source for: // - Hangul Syllables (AC00-D7A3): no other merged font ships Korean glyphs. @@ -9,7 +9,7 @@ namespace HellionChat; // font is Inter-Light (no CJK), so the fallback is the SOLE Han source. The JpRange // overlap is harmless (MergeMode: the Japanese font wins for shared kanji). // Deliberately excluded: ONLY the ASCII/Latin Default block (0x20-0xFF), which the -// global font already owns -- that doubled Latin merge is the B1 waste being removed. +// global font already owns -- that doubled Latin merge is the waste being removed. // Kept as plain start/end pairs so it is unit-testable without the unsafe ImGui // glyph-range builder (mirrors FontSizeResolver's split-for-test rationale). internal static class CjkFallbackRange diff --git a/HellionChat/Configuration.cs b/HellionChat/Configuration.cs index c46c46d..02a0c9f 100755 --- a/HellionChat/Configuration.cs +++ b/HellionChat/Configuration.cs @@ -35,7 +35,7 @@ public class ConfigKeyBind [Serializable] public class Configuration : IPluginConfiguration { - internal const int LatestVersion = 26; + internal const int LatestVersion = 27; public int Version { get; set; } = LatestVersion; @@ -45,16 +45,16 @@ public class Configuration : IPluginConfiguration // Global window opacity, applied across all themes. public float WindowOpacity = 0.85f; - // UI-12: background opacity of the main chat window while unfocused. + // Background opacity of the main chat window while unfocused. // WindowOpacity above stays the focused value. - public float WindowOpacityInactive = 0.65f; + public float WindowOpacityInactive = 0.75f; // Reserved for future UI toggles; pre-declared to avoid a migration later. public bool ReduceMotion; // v1.2.1: default flipped false → true. Compact single-line layout is // more readable than the card-rows layout introduced in v1.2.0. - public bool UseCompactDensity = true; + public bool UseCompactDensity; // Privacy by Default master switch. Set false to restore upstream behaviour. public bool PrivacyFilterEnabled = true; @@ -77,7 +77,7 @@ public class Configuration : IPluginConfiguration .PrivacyDefaults .DefaultPersistUnknownChannels; - // F3.2: dedup unknown-ChatType warnings so a chatty filter doesn't spam + // Dedup unknown-ChatType warnings so a chatty filter doesn't spam // the log every frame. NonSerialized so the warning fires once per // runtime, not once-ever-per-install. [NonSerialized] @@ -107,7 +107,7 @@ public class Configuration : IPluginConfiguration var known = Enum.IsDefined(typeof(ChatType), type); - // F3.2: log first occurrence of a ChatType the running build doesn't + // Log the first occurrence of a ChatType the running build doesn't // recognise — i.e. one a future FFXIV patch may have added. if (!known && !listed && _warnedUnknownChannels.Add(type)) { @@ -145,23 +145,31 @@ public class Configuration : IPluginConfiguration // who don't care, and dodges the per-frame DrawList overhead on low-end // hardware. Gradient (Color3 / GradientColourSet) is parsed but rendered // as the primary Color until a later cycle ports the animation. - public bool ShowHonorificGlow; + public bool ShowHonorificGlow = true; public bool EnableAutoTellTabs = true; public int AutoTellTabsLimit = 15; - public bool AutoTellTabsCompactDisplay; - public int AutoTellTabsHistoryPreload = 20; + public bool AutoTellTabsCompactDisplay = true; + public int AutoTellTabsHistoryPreload = 100; - // Sidebar width in pixels. Default 44 mirrors the icon-only layout from - // v1.2.0; users can widen up to 160 to fit a section-header line like - // "Active Tells (3)" without truncation. - public int SidebarWidth = 44; + // Expanded sidebar width in pixels. 44 was carried over from the v1.2.0 + // icon-only layout and stayed the default long after the sidebar started + // drawing labels beside those icons, so every tab name came out clipped -- + // it only went unnoticed because everyone had widened it by hand. 160 fits + // the German tab names, which are the longest of the 25 languages, and the + // floor below is set where they stop being readable rather than where the + // icons stop fitting. + public int SidebarWidth = 160; public bool AutoTellTabsShowGreetedToggle; public bool SeenPopOutInputHint; public bool PopOutInputEnabled = true; public bool SeenPopOutHeaderHint; - public bool AutoTellTabsOpenAsPopout; - // UI-7: how sender names are rendered in the chat log. + // On by default: the wizard's closing step tells the user to try /tell and + // watch a conversation open on its own, so the behaviour it describes has to + // be the behaviour they get. + public bool AutoTellTabsOpenAsPopout = true; + + // How sender names are rendered in the chat log. public WorldSuffixMode WorldSuffixMode = WorldSuffixMode.OtherWorldOnly; public NameFormMode NameFormMode = NameFormMode.Full; @@ -194,7 +202,7 @@ public class Configuration : IPluginConfiguration // resource sets disagree on what PrettierTimestamps even means -- the wizard // called it "relative time", the settings tab "modern layout". public bool PrettierTimestamps = true; - public bool MoreCompactPretty; + public bool MoreCompactPretty = true; public bool HideSameTimestamps = true; // No reader; see the reconnect backlog. @@ -211,12 +219,12 @@ public class Configuration : IPluginConfiguration public bool OnlyPreviewIf; public int PreviewMinimum = 1; public PreviewPosition PreviewPosition = PreviewPosition.Inside; - public CommandHelpSide CommandHelpSide = CommandHelpSide.None; + public CommandHelpSide CommandHelpSide = CommandHelpSide.Right; public KeybindMode KeybindMode = KeybindMode.Strict; public LanguageOverride LanguageOverride = LanguageOverride.None; public bool CanMove = true; public bool CanResize = true; - public bool ShowTitleBar = true; + public bool ShowTitleBar; public bool ShowPopOutTitleBar = true; public bool DatabaseBattleMessages; public bool FilterIncludePreviousSessions; @@ -232,7 +240,7 @@ public class Configuration : IPluginConfiguration // Toast when a tell the user sent could not be delivered. public bool NotifyFailedTell = true; - // UI-11: warn before sending a message that carries plugin-only glyphs. + // Warn before sending a message that carries plugin-only glyphs. public bool NotifyPluginDisclosure = true; public bool KeepInputFocus = true; public bool Use24HourClock = true; @@ -402,9 +410,9 @@ public class Tab public bool IsTempTab; - // Pinned TempTabs survive plugin reload and logout — tester feedback from - // Jin (v1.4.7). Pinned tabs live in their own pool (MaxPinnedTempTabs) - // separate from the AutoTellTabsLimit bucket. + // Pinned TempTabs survive plugin reload and logout -- tester feedback in + // v1.4.7. Pinned tabs live in their own pool (MaxPinnedTempTabs) separate + // from the AutoTellTabsLimit bucket. public bool IsPinned; public bool AllSenderMessages; public TellTarget TellTarget = TellTarget.Empty(); @@ -462,7 +470,7 @@ public class Tab [NonSerialized] internal string? _cachedTellIcon; - // PM-3 hover-lerp state. Default 0f means "not hovered". Sidebar + // hover-lerp state. Default 0f means "not hovered". Sidebar // path animates per tab; card-mode-border path is tab-aggregate // (any card-row hover ramps the alpha for all cards in this tab). // Lerp speed lives in the render loop, not here, so the same field @@ -944,6 +952,39 @@ public static class LanguageOverrideExt // Mutable.ExtraGlyphRanges so users do not need to know which range // to tick manually. Returns 0 for locales fully covered by the default // ImGui glyph range (Latin-1) or by the separate Japanese font handle. + // The same mapping keyed by culture code, for when the language override is + // None and the UI follows Dalamud. Without this the ranges only ever get + // filled by an explicit language pick -- installs that never touched the + // setting rendered their own locale in whatever the default range covers, + // which for Korean, Chinese, Cyrillic and Greek is boxes. It went unnoticed + // while configs accumulated ranges over time; a fresh config has none. + public static ExtraGlyphRanges RequiredGlyphRangesForCulture(string? cultureCode) + { + var code = (cultureCode ?? string.Empty).ToLowerInvariant(); + + // Longest first: zh-hant has to win over the zh prefix. + if (code.StartsWith("zh-hant") || code.StartsWith("zh-tw") || code.StartsWith("zh-hk")) + return ExtraGlyphRanges.ChineseFull; + if (code.StartsWith("zh")) + return ExtraGlyphRanges.ChineseSimplifiedCommon; + if (code.StartsWith("ko")) + return ExtraGlyphRanges.Korean; + if (code.StartsWith("uk") || code.StartsWith("ru") || code.StartsWith("be")) + return ExtraGlyphRanges.Cyrillic; + if (code.StartsWith("el")) + return ExtraGlyphRanges.Greek; + if ( + code.StartsWith("cs") + || code.StartsWith("pl") + || code.StartsWith("ro") + || code.StartsWith("hu") + || code.StartsWith("tr") + ) + return ExtraGlyphRanges.LatinExtended; + + return 0; + } + public static ExtraGlyphRanges RequiredGlyphRanges(this LanguageOverride mode) => mode switch { diff --git a/HellionChat/FontManager.cs b/HellionChat/FontManager.cs index b9e6013..6a60292 100644 --- a/HellionChat/FontManager.cs +++ b/HellionChat/FontManager.cs @@ -94,13 +94,13 @@ public sealed class FontManager : IDisposable private ushort[] Ranges = []; private ushort[] JpRange = []; - // B1: trimmed remainder the NotoSansCjk fallback is the sole source for + // Trimmed remainder the NotoSansCjk fallback is the sole source for // (Hangul + full Han); excludes the Default/Latin block already merged // by the global font, so the fallback no longer re-merges the full Ranges array. private ushort[] CjkFallbackGlyphRange = []; // Report accessor for the ctor self-test: built glyph-range array lengths so - // the step can show the B1 dedup effect (a small trimmed fallback vs the large + // the step can show the dedup effect (a small trimmed fallback vs the large // primary range) in its on-disk report instead of a bare Pass. internal (int Ranges, int JpRange, int CjkFallback) GlyphRangeLengths => (Ranges.Length, JpRange.Length, CjkFallbackGlyphRange.Length); @@ -247,7 +247,7 @@ public sealed class FontManager : IDisposable // Instance method so Ranges / JpRange are reachable without parameter // plumbing; PascalCase field names follow the existing class style. - // B1: shared CJK + symbols tail for both the regular and italic delegate + // Shared CJK + symbols tail for both the regular and italic delegate // fonts. Earlier-merged fonts win for shared codepoints (imgui MergeMode), // so this runs AFTER the primary font is set as config.MergeFont. The CJK // fallback is the sole Hangul/Simplified-Han source when UseHellionFont=true @@ -265,7 +265,7 @@ public sealed class FontManager : IDisposable config.GlyphRanges = JpRange; AddFontWithFallback(tk, Plugin.Config.JapaneseFontV2.FontId, config, "japanese"); - // NotoSansCjk fallback, trimmed to CjkFallbackGlyphRange (B1). Merged last so earlier fonts win. + // NotoSansCjk fallback, trimmed to CjkFallbackGlyphRange. Merged last so earlier fonts win. config.SizePt = basePt; config.GlyphRanges = CjkFallbackGlyphRange; AddFontWithFallback( @@ -427,7 +427,7 @@ public sealed class FontManager : IDisposable // Common extras (Axis ingame glyphs, endonyms, enclosed alphanumerics) // belong to the primary/Japanese ranges only. The trimmed CJK fallback - // (B1) skips them so it stays a pure Hangul/Simplified-Han remainder and + // skips them so it stays a pure Hangul/Simplified-Han remainder and // does not re-merge the Default-block work the global font already did. if (includeCommonExtras) { @@ -498,7 +498,7 @@ public sealed class FontManager : IDisposable ); JpRange = BuildRange(GlyphRangesJapanese.GlyphRanges, includeCommonExtras: true); - // B1: the fallback gets only the trimmed Hangul/Simplified-Han remainder. + // The fallback gets only the trimmed Hangul/Simplified-Han remainder. // No Default block, no endonyms — those are already merged by the global and // Japanese fonts, so re-merging them on the fallback was wasted atlas work. CjkFallbackGlyphRange = BuildRange(CjkFallbackRange.Pairs, includeCommonExtras: false); diff --git a/HellionChat/GameFunctions/Chat.cs b/HellionChat/GameFunctions/Chat.cs index ccb14f7..b12741d 100755 --- a/HellionChat/GameFunctions/Chat.cs +++ b/HellionChat/GameFunctions/Chat.cs @@ -234,7 +234,7 @@ internal sealed unsafe class Chat : IDisposable // Seed the just-typed character into our input field and focus it, the // same InputBar.AppendPending + Activate prefill path inventory item-links - // use. Prefill-only — no tab switch (Flo decision 2026-06-15). + // use. Prefill only, deliberately: no tab switch. if (input != null) { Plugin.InputBar.AppendPending(input); @@ -355,9 +355,9 @@ internal sealed unsafe class Chat : IDisposable if (playerName != null) { // Right-click -> Send Tell: prefill our input the same way our own - // "Send Tell" payload menu does (PayloadHandler), then focus. Prefill- - // only — no tab switch, no ChatActivatedArgs revival (Flo decision - // 2026-06-15). The game supplies worldName here, so no sheet lookup. + // "Send Tell" payload menu does (PayloadHandler), then focus. Prefill + // only, deliberately: no tab switch, no ChatActivatedArgs revival. + // The game supplies worldName here, so no sheet lookup. PrefillTellInput( playerName->ToString(), worldName != null ? worldName->ToString() : null @@ -393,7 +393,7 @@ internal sealed unsafe class Chat : IDisposable { // In-foray right-click -> Send Tell: same prefill path as the non-foray // tell. The foray-specific TellSpecial channel routing stays deferred - // (v1.8.1, SetEurekaTellChannel) — prefill-only here (Flo decision 2026-06-15). + // (v1.8.1, SetEurekaTellChannel) -- prefill only here as well. PrefillTellInput( playerName->ToString(), worldName != null ? worldName->ToString() : null diff --git a/HellionChat/GameFunctions/KeybindManager.cs b/HellionChat/GameFunctions/KeybindManager.cs index fa47706..1feb9ed 100644 --- a/HellionChat/GameFunctions/KeybindManager.cs +++ b/HellionChat/GameFunctions/KeybindManager.cs @@ -507,14 +507,14 @@ internal unsafe class KeybindManager : IDisposable // Resolve the surface this keybind acts on FIRST: a focused pop-out otherwise // the main window. Channel-set/REPLY/prefill all write here so the action - // follows the input the user is typing in (C3 full tail rebuild, OD-1). + // follows the input the user is typing in. var (targetWindow, targetTab) = ResolveKeybindTarget(); // Surface + focus the resolved target ONCE, before routing. Main: ActivateChat // re-surfaces it from a hide/closed state (the chat-activation entry point // retired in v1.6.0). Pop-out: arm only its focus — NOT ActivateChat, which // would yank the main window to front and un-hide it on every pop-out-targeted - // keybind (OD-1: stay where the user types). Exactly one window arms focus per + // keybind: stay where the user types. Exactly one window arms focus per // keybind, so the next frame has no SetKeyboardFocusHere race. if (targetWindow is ChannelPopoutWindow) targetWindow.RequestInputFocus(); @@ -530,7 +530,7 @@ internal unsafe class KeybindManager : IDisposable { // Direct channel-switch binds (CMD_SAY/PARTY/numbered linkshells/…): switch // the game channel AND mirror it onto the resolved tab so the input pill - // shows the real send target (pill-sync, Flo decision 2026-06-15). + // shows the real send target (pill-sync). Plugin.Instance.Functions.Chat.SetChannel(channel); // Only mirror onto the tab when the game actually accepted the switch — an // empty linkshell slot leaves the game channel untouched, so the pill must @@ -547,7 +547,7 @@ internal unsafe class KeybindManager : IDisposable // Rotation binds (REPLY / linkshell-cycle). Ported from v1.5.6's // ChatLogWindow.Activated (1d3b429:240-334) without the ChatActivatedArgs // indirection (gone in the rewrite). Writes onto the resolved surface's - // tab (C2/C3 shared target), not Plugin.CurrentTab. + // tab, not Plugin.CurrentTab. if (targetTab is { } rotTab) { var targetChannel = (InputChannel?)rotateChannel; @@ -629,7 +629,7 @@ internal unsafe class KeybindManager : IDisposable if (info.Permanent) { - // KB-01 (1.5.6 parity, ChatLogWindow.SetChannel 1d3b429:1476-1479): + // 1.5.6 parity (ChatLogWindow.SetChannel, 1d3b429:1476-1479): // committing the game channel also pre-targets the game's native input. // Forward the tab's reply target for Tell so the partner is armed // game-side (ChangeChatChannel code 17); null for a linkshell — @@ -654,7 +654,7 @@ internal unsafe class KeybindManager : IDisposable // Prefill text binds (CMD_COMMAND seeds "/"): the token always goes to the // main InputBar (the focus contract does not expose pop-out buffers); a // focused pop-out already received focus above, so only token routing matters - // here (documented scope limit, OD-1). + // here -- a documented scope limit. if (info.Text is { } text) Plugin.Instance.InputBar.SetPendingMessage(text); } @@ -665,7 +665,7 @@ internal unsafe class KeybindManager : IDisposable } // Resolve which chat surface a keybind action targets: the open pop-out whose - // input currently has focus, otherwise the main window. C2/C3 share this so a + // input currently has focus, otherwise the main window. Both paths share it so a // channel-switch/REPLY/prefill follows the surface the user is typing in. The // returned tab is that surface's bound tab (pop-out: Bound; main: ActiveTab). // Null tab => skip the tab-write (early-load window where no tab exists yet). @@ -682,7 +682,7 @@ internal unsafe class KeybindManager : IDisposable } // Tab-delta keybinds (ChatTabForward/Backward) stay main-window-only by design: - // a channel-bound pop-out has no tab list to cycle (OD-1). The focus contract is + // a channel-bound pop-out has no tab list to cycle. The focus contract is // consumed by the channel-set/REPLY/prefill tail, not here. private void DispatchTabDelta(int delta) { diff --git a/HellionChat/HellionChat.csproj b/HellionChat/HellionChat.csproj index 2c7b7d6..d91a870 100644 --- a/HellionChat/HellionChat.csproj +++ b/HellionChat/HellionChat.csproj @@ -1,7 +1,7 @@ - 1.14.0 + 2.0.0 enable enable diff --git a/HellionChat/HellionChat.yaml b/HellionChat/HellionChat.yaml index 28f9e9b..b7aa277 100755 --- a/HellionChat/HellionChat.yaml +++ b/HellionChat/HellionChat.yaml @@ -35,6 +35,39 @@ tags: - Replacement - Privacy changelog: |- + **v2.0.0 — Rebuilt, Repaired, Reset (2026-08-19)** + + Nine development cycles in one release. Everything built as v1.6.0 through v1.15.0 ships here; those versions were never published on their own. + + **This update resets your settings.** Nine cycles of rebuilding left saved values pointing at surfaces that no longer exist, and starting over is the only way to be sure every install is on the same defaults. **Your message history is untouched** — it lives in a separate database. Your old settings are kept next to the config file as `HellionChat.json.pre-2.0.0.bak`. + + Fixed, and several of these lost data or hid it: + + - Retroactive cleanup could never be applied at all. The preview took the database lock itself and that counted as a change, so every preview went stale the instant it finished and the apply button never appeared. + - Compacting the database ran against an open reader and failed, after which the plugin reported that nothing had been deleted — while everything had. + - Deleting messages left them in the search index, so search kept returning rows that were already gone. + - Pinned tell tabs came up empty for a whole session: the history query ran at plugin start, before any character is logged in, and never tried again. + - Export wrote invalid JSON where a setting value was involved, and a byte-order mark that strict parsers reject. + - A pop-out with its title bar switched on could not be closed, and a tell from a popped-out partner hijacked the main window's active tab. + - One third-party emote service started returning 403, and that response took all 65 working global emotes down with it on every start. + - The GDPR notice for the full-history profile had been translated into 25 languages and shown nowhere since May. + + Changed, and one of these changes what gets stored: + + - **The channel grid is now authoritative.** Until now the unknown-channel failsafe was applied to known channels too, so a channel you had unticked was still being written while that failsafe was on. If that was you, this release stores less than before. Nothing already in the database is touched. + - Every window is drawn by the plugin rather than by ImGui defaults, and they share one visual language: structure carried by typography, a surface on anything you can press, and colours measured against what sits behind them. + - Typography has named roles — sender, body and timestamp mean the same thing everywhere, timestamps sit in their own column, system messages are italic. + - Export, the tab editor, database maintenance and pinning had lost their entry points during the rebuild and are reachable again. + - Screenshot mode reached one of four surfaces that draw a tab name. It reaches all four now. + + Removed: six settings that had a control and a saved value but no reader anywhere in the plugin; a per-tab regex filter the game's own blackword filter covers; and a wizard checkbox that was collected, reported as applied and never read. + + New: an Emote tab in the default layout, local and server clocks in the status bar, a screenshot mode reachable from the input row, `/hellion wizard` to reopen the setup wizard, a style lab under `/hellion lab`, and 25 UI languages with the settings window and wizard fully covered. + + Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2). The two codebases have diverged far enough that they no longer line up. + + --- + **v1.5.6 — Settings Overhaul + Filter & Notification Polish (2026-05-23)** - Settings window reorganised: ten tabs down to seven (General, Appearance, Chat, Window, Channels, Data & Privacy, About). Each tab now uses collapsible sections grouped by control type. Sections start collapsed every time you open a tab — less noise, easier to find what you need. @@ -116,65 +149,4 @@ changelog: |- Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2). - --- - - **v1.5.3 — Localisation Wave + Bundled-Font Overhaul (2026-05-19)** - - Multi-language pass plus a long-standing first-frame HITCH lands - as a side effect of a font-stack rewrite. - - User-visible: - - - 24 selectable UI languages (was 2). Catalan, Czech, Danish, - Dutch, English, Finnish, French, German, Greek, Hungarian, - Italian, Japanese, Korean, Norsk bokmål, Polish, Portuguese - (BR + PT), Romanian, Russian, Spanish, Swedish, Turkish, - Ukrainian, Simplified + Traditional Chinese. Sorted by endonym, - "None" pinned first. Non-native locales are AI-assisted and - flagged for native-speaker review via the Forge Discord. - - Bundled Inter Light replaces Exo 2 (SIL OFL 1.1, 343 KB). The - Inter font ships Latin Extended-A/B, Greek polytonic and - Cyrillic Supplement coverage; NotoSansCjkRegular joins as a - third merge layer for Hangul and Simplified-Han glyphs the - FFXIV Japanese game font does not ship. - - First-frame HITCH dropped from ~74 ms (v1.5.2 baseline that - held since v1.4.x) to a median of ~20 ms (5-reload sample - 17.9-23.6 ms, Linux/Wine). The bundled-font path silently - fell back to the FFXIV Axis font for the entire v1.5.x series - because of an early-return in the draw loop. The fix that - routes RegularFont through draw also lands the defer-pattern - win the v1.5.1 cycle was reaching for. - - ExtraGlyphRanges auto-activates on language change. Korean, - ChineseFull and the two new flags (LatinExtended, Greek) toggle - on without a manual visit to Fonts and Colours. - - New WarningText under the language dropdown notes FFXIV's - chat input only fully supports EN/DE/FR/JA character sets. - Other languages render in HellionChat but may garble when - typed into in-game chat. - - Under the hood: - - - Three-layer font stack: Inter Light primary, FFXIV - JapaneseFont merge 1 for kana/kanji style, NotoSansCjkRegular - merge 2 for everything else CJK. - - LanguageOverride enum gains ten locales plus three previously - commented out (Italian, Korean, Norwegian as `nb`). New - values append to the enum so existing config integers stay - stable across update. - - Crowdin gap closed: four post-sync ChatTwo keys backfilled - into 13 legacy locales with per-key AI markers. - - Plugin.LoadAsync runs a one-shot migration that ORs in the - matching ExtraGlyphRanges flag for users already on a - non-default language. Settings.Apply auto-activates on - change going forward. - - Em-dash sweep across the EN source and 18 translations to the - house style. Russian and Ukrainian keep the typographic norm. - - Migration v17 stays. UseHellionFont users transition from Exo 2 - to Inter Light transparently on first reload. - - Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2). - - --- - - Full history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases + Earlier history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases diff --git a/HellionChat/Infrastructure/Hosting/InitHostedServices.cs b/HellionChat/Infrastructure/Hosting/InitHostedServices.cs index 65d36a8..4f225d7 100644 --- a/HellionChat/Infrastructure/Hosting/InitHostedServices.cs +++ b/HellionChat/Infrastructure/Hosting/InitHostedServices.cs @@ -135,7 +135,7 @@ internal sealed class PayloadHandlerInitHostedService( { public async Task StartAsync(CancellationToken cancellationToken) { - // §6.2 cycle-resolution: both singletons exist by the time HostedServices + // Cycle resolution: both singletons exist by the time HostedServices // run, so this is the first safe point to wire the setter. messageList.AttachPayloadHandler(payloadHandler); @@ -172,7 +172,7 @@ internal sealed class PayloadHandlerInitHostedService( // InputBar -> CommandHelpWindow -> MainWindow -> InputBar (MS.DI does not catch // it through FactoryCallSite registrations and the resolve recurses silently). // Both singletons exist by host.StartAsync time, so this is the first safe point -// to wire the setter — same §6.2 pattern as MessageList.AttachPayloadHandler. +// to wire the setter — same setter-injection pattern as MessageList.AttachPayloadHandler. internal sealed class CommandHelpWindowInitHostedService( CommandHelpWindow commandHelpWindow, MainWindow mainWindow @@ -190,7 +190,7 @@ internal sealed class CommandHelpWindowInitHostedService( // Attaches the singleton PayloadHandler to every pre-allocated pop-out // window's MessageList post-container-build. Pool/window cannot take the // PayloadHandler via ctor (that would close the silent FactoryCallSite cycle — -// same §6.2 reason as MessageList.AttachPayloadHandler / CommandHelpWindow. +// same setter-injection reason as MessageList.AttachPayloadHandler / CommandHelpWindow. // AttachMainWindow). Both singletons exist by host.StartAsync time. internal sealed class ChannelPopoutInitHostedService( ChannelPopoutPool pool, diff --git a/HellionChat/Ipc/ExtraChat.cs b/HellionChat/Ipc/ExtraChat.cs index f69547c..1751f49 100644 --- a/HellionChat/Ipc/ExtraChat.cs +++ b/HellionChat/Ipc/ExtraChat.cs @@ -31,7 +31,7 @@ public sealed class ExtraChat : IDisposable // volatile: IPC callbacks fire on a Dalamud thread while ImGui reads these. // Reference assignment is atomic on x64, but the barrier ensures visibility - // across threads (especially Mono/Wine). See AUDIT-2026-05-05 [SEC-01]. + // across threads (especially Mono/Wine). Raised in the 2026-05-05 audit. private volatile Dictionary ChannelCommandColoursInternal = new(); internal IReadOnlyDictionary ChannelCommandColours => ChannelCommandColoursInternal; diff --git a/HellionChat/Ipc/TypingIpc.cs b/HellionChat/Ipc/TypingIpc.cs index 57d36b2..4e81848 100644 --- a/HellionChat/Ipc/TypingIpc.cs +++ b/HellionChat/Ipc/TypingIpc.cs @@ -20,7 +20,7 @@ internal sealed class TypingIpc : IDisposable private ICallGateProvider StateQueryGate { get; } private ICallGateProvider StateChangedGate { get; } - // v1.4.9 R4: ChatTwo IPC compatibility mirror. Some third-party plugins + // v1.4.9: ChatTwo IPC compatibility mirror. Some third-party plugins // have a no-fork policy and subscribe only to ChatTwo.*-prefixed IPC // gates. HellionChat replaces ChatTwo (conflict detection prevents // parallel loading), so mirroring the ChatTwo provider slots lets those @@ -50,7 +50,7 @@ internal sealed class TypingIpc : IDisposable "HellionChat.ChatInputStateChanged" ); - // v1.4.9 R4: ChatTwo-prefixed compatibility slots (see class-level comment). + // v1.4.9: ChatTwo-prefixed compatibility slots (see class-level comment). ChatTwoStateQueryGate = Plugin.Interface.GetIpcProvider( "ChatTwo.GetChatInputState" ); @@ -102,7 +102,7 @@ internal sealed class TypingIpc : IDisposable HasState = true; LastState = state; StateChangedGate.SendMessage(state); - // v1.4.9 R4: mirror on ChatTwo-prefixed slot for no-fork-policy plugins. + // v1.4.9: mirror on ChatTwo-prefixed slot for no-fork-policy plugins. ChatTwoStateChangedGate.SendMessage(state); } diff --git a/HellionChat/IpcManager.cs b/HellionChat/IpcManager.cs index c8cfbd6..2bf6959 100755 --- a/HellionChat/IpcManager.cs +++ b/HellionChat/IpcManager.cs @@ -22,7 +22,7 @@ internal sealed class IpcManager : IDisposable object? > InvokeGate { get; } - // v1.4.9 R4: ChatTwo IPC compatibility mirror. Third-party plugins with + // v1.4.9: ChatTwo IPC compatibility mirror. Third-party plugins with // a no-fork policy (e.g. Artisan, AllaganTools) only subscribe to the // ChatTwo.*-prefixed context-menu integration gates. Mirroring all four // provider slots under the ChatTwo namespace lets those plugins keep @@ -65,7 +65,7 @@ internal sealed class IpcManager : IDisposable object? >("HellionChat.Invoke"); - // v1.4.9 R4: ChatTwo-prefixed mirrors of the four context-menu slots + // v1.4.9: ChatTwo-prefixed mirrors of the four context-menu slots // above. Share the same Register/Unregister backing methods so a // plugin that subscribes via either namespace lands in the same // Registered list. SendMessage on Invoke fans out to both gates. @@ -103,7 +103,7 @@ internal sealed class IpcManager : IDisposable ) { InvokeGate.SendMessage(id, sender, contentId, payload, senderString, content); - // v1.4.9 R4: fan out the same event to plugins listening on ChatTwo.Invoke. + // v1.4.9: fan out the same event to plugins listening on ChatTwo.Invoke. ChatTwoInvokeGate.SendMessage(id, sender, contentId, payload, senderString, content); } diff --git a/HellionChat/MessageManager.cs b/HellionChat/MessageManager.cs index 31f8b1d..c44bf9f 100644 --- a/HellionChat/MessageManager.cs +++ b/HellionChat/MessageManager.cs @@ -163,7 +163,7 @@ internal class MessageManager : IAsyncDisposable internal void ClearAllTabs() { - // B3: snapshot the tab LIST under the shared lock so the worker-thread + // Snapshot the tab LIST under the shared lock so the worker-thread // add/remove can't tear the enumeration; tab.Clear() then runs lock-free // (each tab's Messages has its own SemaphoreSlim — lock order: list outer). List tabsSnapshot; @@ -184,8 +184,8 @@ internal class MessageManager : IAsyncDisposable using var messages = Store.GetMostRecentMessages(CurrentContentId, since); // TempTabs excluded (live state from AutoTellTabsService). Bucket via the - // pure MapMessagesToTabs so the assignment stays testable outside Dalamud (B3-1). - // B3: snapshot under the shared lock (list copy only — short critical + // pure MapMessagesToTabs so the assignment stays testable outside Dalamud. + // Snapshot under the shared lock (list copy only — short critical // section). The Store query above and the AddSortPrune writes below stay // OUTSIDE the lock (lock order: list outer, MessageList inner). List nonTempTabs; @@ -248,9 +248,8 @@ internal class MessageManager : IAsyncDisposable _logger.LogError(ex, "Error in FilterAllTabs"); } - // v1.4.9 R3 profiling: Information so the xllog tail surfaces this - // without a Debug filter. Belt-and-suspenders for future plugin-load - // regressions; remains in place after Sub-Task 3.4 Befund. + // Information, not Debug, so the xllog tail surfaces this without a + // filter. Kept as a guard against future plugin-load regressions. _logger.LogInformation($"FilterAllTabs took {stopwatch.ElapsedMilliseconds}ms"); }); } @@ -436,10 +435,10 @@ internal class MessageManager : IAsyncDisposable // TEST-MIRROR: ../_Helpers/TabSoundDecision.cs // Unseen ("count only what you haven't seen") suppresses unread on an inactive // tab when the active tab ALSO shows this message — you already saw it in the - // tab you're looking at (1.5.6 / upstream ChatTwo behavior). Pre-F2 the "active - // tab" was wrongly pinned to Tabs[0], so this fired against the wrong tab; F2 - // recoupled CurrentTab to the REAL active tab, so currentTabMatches is now - // measured against the tab you actually see. All -> always counts; None -> + // tab you're looking at (1.5.6 / upstream ChatTwo behavior). The "active tab" + // used to be pinned to Tabs[0], so this fired against the wrong one until + // CurrentTab was recoupled to the real active tab, and currentTabMatches is + // now measured against the tab you see. All -> always counts; None -> // counts here and is gated out at the display layer. Pure + SelfTest-able. internal static bool ShouldCountUnread(Tab tab, Tab currentTab, bool currentTabMatches) => !( diff --git a/HellionChat/MessageStore.cs b/HellionChat/MessageStore.cs index b0c6f5a..ab97840 100644 --- a/HellionChat/MessageStore.cs +++ b/HellionChat/MessageStore.cs @@ -245,7 +245,7 @@ internal class MessageStore : IDisposable private SqliteConnection Connect() { - // v1.4.9 R3 profiling: trace cost of SQLite open + pragma-apply. Paired + // v1.4.9 profiling: trace cost of SQLite open + pragma-apply. Paired // with the Migrate-Stopwatch below — Connect alone is the cheap half // (Open + a handful of PRAGMAs); the expensive half typically lives in // Migrate, especially on a large DB after a schema bump. @@ -260,7 +260,7 @@ internal class MessageStore : IDisposable private void Migrate() { - // v1.4.9 R3 profiling: trace cost of the schema-migration chain. On a + // v1.4.9 profiling: trace cost of the schema-migration chain. On a // large DB after a fresh schema bump this is the dominant SQLite cost // at plugin-load, not Connect. var migrateSw = System.Diagnostics.Stopwatch.StartNew(); @@ -939,8 +939,7 @@ internal class MessageStore : IDisposable // storage form on both sides so the IN(...) compare matches. SQLite has a // hard parameter limit of 999 in default builds, so we chunk the input -- // a 1000-hit FTS query never explodes the SELECT. Result ordering is not - // guaranteed; callers re-sort (e.g. DbViewer sorts by Date descending in - // Sub-Task 4.4). + // guaranteed; callers re-sort (DbViewer sorts by Date descending). public IReadOnlyList LoadByGuids(IReadOnlyList guidStrings) { if (guidStrings.Count == 0) diff --git a/HellionChat/NameDisplayModes.cs b/HellionChat/NameDisplayModes.cs index 4f71448..542471c 100644 --- a/HellionChat/NameDisplayModes.cs +++ b/HellionChat/NameDisplayModes.cs @@ -2,7 +2,7 @@ using HellionChat.Resources; namespace HellionChat; -// UI-7: how a sender's name is rendered in the chat log. Kept in its own file +// How a sender's name is rendered in the chat log. Kept in its own file // (no Dalamud usings) so the SenderNameFormatter pure-helper test stays // AppDomain-isolated (feedback_dalamud_test_isolation). diff --git a/HellionChat/PayloadHandler.cs b/HellionChat/PayloadHandler.cs index 82143d8..2d3d95b 100644 --- a/HellionChat/PayloadHandler.cs +++ b/HellionChat/PayloadHandler.cs @@ -263,8 +263,8 @@ internal sealed class PayloadHandler // Eureka, Bozja and Occult need special handling as tells work different if (!Sheets.IsInForay()) { - // §6.9: single SetPendingMessage call; v1.5.6 used incremental Chat += writes. - // XC-8: shares the /tell builder with the native detours. IsPublic (not + // Single SetPendingMessage call; v1.5.6 used incremental Chat += writes. + // Shares the /tell builder with the native detours. IsPublic (not // IsNullOrEmpty) is resolved HERE — a private/null world must NOT leak @World. _inputBar.SetPendingMessage( GameFunctions.Chat.BuildTellCommand( @@ -393,7 +393,7 @@ internal sealed class PayloadHandler var inputChannel = chunk.Message?.Code.Type.ToInputChannel(); if (inputChannel != null && ImGui.Selectable(Language.Context_ReplyInSelectedChatMode)) { - // §6.3: route channel-switch through MainWindow's active tab + // Route the channel switch through MainWindow's active tab _mainWindow.ActiveTab?.CurrentChannel?.SetChannel(inputChannel.Value); _inputBar.Activate = true; } @@ -731,7 +731,7 @@ internal sealed class PayloadHandler using (ImRaii.Tooltip()) using (ImRaii.TextWrapPos(0.0f)) using ( - // §4.2: use active theme text colour instead of the former LogWindow.DefaultText static. + // Use the active theme text colour instead of the former LogWindow.DefaultText static. ImRaii.PushColor( ImGuiCol.Text, ColourUtil.RgbaToVector4(_themes.Active.Colors.TextPrimary) diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index 99aadc6..dea9032 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -134,7 +134,7 @@ public sealed class Plugin : IAsyncDalamudPlugin internal Integrations.HonorificService HonorificService { get; private set; } = null!; internal Integrations.CustomAudioPlayer CustomAudioPlayer { get; private set; } = null!; - // Ctor-smoke anchors (B0-2). Exposed so the Payload/Chunk ctor-smoke steps + // Ctor-smoke anchors. Exposed so the Payload/Chunk ctor-smoke steps // can drive the real per-frame Lender path (Borrow()) and the eager // singletons through the container, never via new(). Mirror of the // FontManager property pattern — every SelfTest reaches services this way. @@ -178,7 +178,7 @@ public sealed class Plugin : IAsyncDalamudPlugin internal bool ChatActivationRequested; // Set in the first DisposeAsync statement so async callbacks scheduled - // via Framework.RunOnTick (v1.4.8 B3 retention sweep) can early-bail + // via Framework.RunOnTick (v1.4.8 retention sweep) can early-bail // before they touch state that has already been torn down. Volatile // because the tick reads it from a different thread than the writer. private volatile bool _isDisposing; @@ -188,9 +188,9 @@ public sealed class Plugin : IAsyncDalamudPlugin // just unloaded belongs to nobody. internal bool IsDisposing => _isDisposing; - // v1.9.0 B5: last full Draw() wall-time in ms, written once per frame at + // v1.9.0: last full Draw() wall-time in ms, written once per frame at // the end of the UiBuilder.Draw handler. Covers the GlobalStyleScope push - // and the font push (§7.5 First-Frame-HITCH must include atlas/style + // and the font push (the first-frame hitch measurement must include atlas/style // prologue cost), not just WindowSystem.Draw — measuring the inner call // alone would drop the prologue and make the figure non-comparable to the // v1.5.6 baseline. Only accumulated here; the disk write happens in @@ -214,7 +214,7 @@ public sealed class Plugin : IAsyncDalamudPlugin // must never block doing so. internal readonly Util.DbOperationGate DbOperations = new(); - // B3: neutral owner of the Config.Tabs LIST-structure lock so both the + // Neutral owner of the Config.Tabs LIST-structure lock so both the // worker-thread mutator (AutoTellTabsService) and the framework-thread // refilter (MessageManager) share ONE monitor. Lock order: this outer, // MessageList's SemaphoreSlim inner — never the reverse. @@ -287,83 +287,118 @@ public sealed class Plugin : IAsyncDalamudPlugin + "Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.10." ); } - // v23 migration: SidebarTabView was the 1.5.6 sidebar↔top-tabs switch, - // superseded by MainWindowLayoutMode in the v1.6.0 rewrite. A user who - // set it false (only effective in 1.5.6) wanted top tabs — carry that - // intent forward. Runs only for pre-v23 configs; fresh configs load at - // LatestVersion and skip it. Additive v20/v22 fields keep their - // initializer defaults as before. - if (Config.Version < 23 && !Config.SidebarTabView) + // 2.0.0 does not migrate, it starts over. Five cycles rebuilt the whole + // window layer, and a config carried through them keeps values chosen + // against surfaces that no longer exist -- an opacity picked for a + // window that has been redrawn twice since, tabs laid out for a sidebar + // that works differently now. Every user of this build is a tester who + // was told this happens, and it is the only way to be sure everyone + // sees the same defaults. + // + // The file is copied aside first. Rolling back to 1.5.6 is a supported + // move here and it stays cheap: the old settings are a file copy away, + // rather than an evening of clicking them back in. + if (Config.Version < 27) { - Config.MainWindowLayoutMode = MainWindowLayoutMode.TopTabs; - } + BackUpConfigBeforeReset(); + + Config = Configuration.CreateFresh(); + Config.Version = 27; + + // Saved immediately: a crash between here and the first user-driven + // save would otherwise run the whole reset again on the next start, + // and the second run would back up the already-reset file over the + // real backup. + SaveConfig(); - // v24 migration: the privacy filter used to route a known but unticked - // channel through the unknown-type failsafe, so the channel grid was - // inert whenever that failsafe was on. Corrected in v1.12.0. A config - // that never picked a channel was storing everything through that hole, - // and the corrected rule would store nothing at all -- so the intent is - // carried forward as a filter that is honestly switched off. - if ( - Config.Version < 24 - && Privacy.StorageRule.ShouldDisableFilterOnV24( - Config.PrivacyFilterEnabled, - Config.PrivacyPersistUnknownChannels, - Config.PrivacyPersistChannels.Count - ) - ) - { - Config.PrivacyFilterEnabled = false; - // Log, not LogProxy: this runs in Phase-0 and the proxy is only - // resolved from the container further down. Log.Information( - "Privacy filter switched off during the v24 migration: it was on with no channels " - + "picked, which stored everything through the unknown-channel failsafe. Pick " - + "channels in Settings to switch it back on." + "Config reset to defaults for 2.0.0. Previous settings kept as " + + "HellionChat.json.pre-2.0.0.bak next to the config file." ); } - - // v25 carried no migration step; the bump was documentation. - // - // v26 does. NameCameFromPartner is what screenshot mode reads to decide - // whether a tab name is a person, and a config written before it existed - // has it false on every tab -- including pinned tell tabs, which survive - // reloads and are named "Player@World". Anything still carrying a tell - // binding or the temp flag got its name from a partner, so the flag is - // set from those two. - // - // Tabs promoted before this version are past saving: promotion clears - // both markers and keeps the name, so nothing in the stored data says - // where that name came from. Renaming one clears the flag anyway, which - // is the same outcome the user gets by editing it. - if (Config.Version < 26) + else { - var carried = 0; - foreach (var tab in Config.Tabs) + // v23 migration: SidebarTabView was the 1.5.6 sidebar↔top-tabs switch, + // superseded by MainWindowLayoutMode in the v1.6.0 rewrite. A user who + // set it false (only effective in 1.5.6) wanted top tabs — carry that + // intent forward. Runs only for pre-v23 configs; fresh configs load at + // LatestVersion and skip it. Additive v20/v22 fields keep their + // initializer defaults as before. + if (Config.Version < 23 && !Config.SidebarTabView) { - if (tab.NameCameFromPartner || (!tab.IsTempTab && tab.TellTarget?.IsSet() != true)) - continue; - - tab.NameCameFromPartner = true; - carried++; + Config.MainWindowLayoutMode = MainWindowLayoutMode.TopTabs; } - if (carried > 0) + // v24 migration: the privacy filter used to route a known but unticked + // channel through the unknown-type failsafe, so the channel grid was + // inert whenever that failsafe was on. Corrected in v1.12.0. A config + // that never picked a channel was storing everything through that hole, + // and the corrected rule would store nothing at all -- so the intent is + // carried forward as a filter that is honestly switched off. + if ( + Config.Version < 24 + && Privacy.StorageRule.ShouldDisableFilterOnV24( + Config.PrivacyFilterEnabled, + Config.PrivacyPersistUnknownChannels, + Config.PrivacyPersistChannels.Count + ) + ) { + Config.PrivacyFilterEnabled = false; + // Log, not LogProxy: this runs in Phase-0 and the proxy is only + // resolved from the container further down. Log.Information( - $"Marked {carried} tab(s) as partner-named during the v26 migration, so " - + "screenshot mode hides them in the channel header." + "Privacy filter switched off during the v24 migration: it was on with no channels " + + "picked, which stored everything through the unknown-channel failsafe. Pick " + + "channels in Settings to switch it back on." ); } + + // v25 carried no migration step; the bump was documentation. + // + // v26 does. NameCameFromPartner is what screenshot mode reads to decide + // whether a tab name is a person, and a config written before it existed + // has it false on every tab -- including pinned tell tabs, which survive + // reloads and are named "Player@World". Anything still carrying a tell + // binding or the temp flag got its name from a partner, so the flag is + // set from those two. + // + // Tabs promoted before this version are past saving: promotion clears + // both markers and keeps the name, so nothing in the stored data says + // where that name came from. Renaming one clears the flag anyway, which + // is the same outcome the user gets by editing it. + if (Config.Version < 26) + { + var carried = 0; + foreach (var tab in Config.Tabs) + { + if ( + tab.NameCameFromPartner + || (!tab.IsTempTab && tab.TellTarget?.IsSet() != true) + ) + continue; + + tab.NameCameFromPartner = true; + carried++; + } + + if (carried > 0) + { + Log.Information( + $"Marked {carried} tab(s) as partner-named during the v26 migration, so " + + "screenshot mode hides them in the channel header." + ); + } + } } - Config.Version = 26; + Config.Version = 27; // Unpinned TempTabs are session-only and dropped on every load. Pinned - // TempTabs survive reload — Jin's tester feedback (v1.4.7). + // TempTabs survive reload -- tester feedback in v1.4.7. Config.Tabs.RemoveAll(TabLifecycleHelpers.ShouldStripOnLoad); - // GP-04: clear stale Tab.PopOut flags now — the pool binds further down + // Clear stale Tab.PopOut flags now — the pool binds further down // (ChannelPopoutPool resolve below), so at this point no tab can own a // slot. A persisted PopOut=true (notably on surviving pinned TempTabs) // would otherwise be a flag with no window. Runs after the strip, before @@ -378,7 +413,10 @@ public sealed class Plugin : IAsyncDalamudPlugin // that path. ORing in the required flag here lets the first atlas // build pick it up, so an upgrade from v1.5.2 renders correctly // without forcing the user to toggle the language twice. - var requiredRanges = Config.LanguageOverride.RequiredGlyphRanges(); + var requiredRanges = + Config.LanguageOverride is LanguageOverride.None + ? LanguageOverrideExt.RequiredGlyphRangesForCulture(Interface.UiLanguage) + : Config.LanguageOverride.RequiredGlyphRanges(); if (requiredRanges != 0 && !Config.ExtraGlyphRanges.HasFlag(requiredRanges)) Config.ExtraGlyphRanges |= requiredRanges; @@ -462,7 +500,7 @@ public sealed class Plugin : IAsyncDalamudPlugin FirstRunWizard = _host.Services.GetRequiredService(); ChannelPopoutPool = _host.Services.GetRequiredService(); - // Ctor-smoke anchors (B0-2). Resolved last, against the fully built + // Ctor-smoke anchors. Resolved last, against the fully built // container: every MakePayloadHandler dep (MainWindow, InputBar, // ChunkRenderer, ...) is resolvable here, and the ChunkRenderer resolve // below just reuses the same cached singleton. These are plain @@ -485,6 +523,7 @@ public sealed class Plugin : IAsyncDalamudPlugin { Config.Tabs.Add(TabsUtil.VanillaGeneral); Config.Tabs.Add(TabsUtil.HellionSystem); + Config.Tabs.Add(TabsUtil.HellionEmote); Config.Tabs.Add(TabsUtil.HellionFreeCompany); Config.Tabs.Add(TabsUtil.HellionParty); Config.Tabs.Add(TabsUtil.HellionLinkshell); @@ -521,7 +560,7 @@ public sealed class Plugin : IAsyncDalamudPlugin new SelfTests.SettingsWindowOpenStep(this), new SelfTests.OnOpenMainUiRoutesMainWindowStep(this), new SelfTests.TypingIpcStateStep(this), - new SelfTests.ConfigMigrationV26Step(this), + new SelfTests.ConfigMigrationV27Step(this), new SelfTests.DbGateWiringStep(this), new SelfTests.ChannelPopoutBindStep(this), new SelfTests.HoverStateFootprintStep(), @@ -599,7 +638,7 @@ public sealed class Plugin : IAsyncDalamudPlugin _ = Task.Run( async () => { - // FQN: Plugin.Notification (Z.74) shadows the type name. + // FQN: the Plugin.Notification property shadows the type name. Dalamud.Interface.ImGuiNotification.IActiveNotification? notif = null; try { @@ -732,7 +771,7 @@ public sealed class Plugin : IAsyncDalamudPlugin return; // Set before any cleanup so deferred Framework.RunOnTick callbacks - // (B3 retention sweep) see the flag and bail out before they touch + // (the retention sweep) see the flag and bail out before they touch // MessageManager / Log / static fields that the rest of this method // is about to tear down. _isDisposing = true; @@ -840,6 +879,38 @@ public sealed class Plugin : IAsyncDalamudPlugin return failure; } + // Copies the config aside before the 2.0.0 reset overwrites it. Best effort + // by design: a backup that cannot be written must not stop the plugin from + // starting, and the reset itself is what the user was told would happen. + // + // Overwrite=false, so a second run cannot bury the real backup under a copy + // of the already-reset file. The reset saves immediately for the same + // reason, but a crash in between is exactly when this matters. + private static void BackUpConfigBeforeReset() + { + try + { + var dir = Interface.ConfigDirectory.Parent?.FullName; + if (dir is null) + return; + + var configFile = Path.Combine(dir, "HellionChat.json"); + if (!File.Exists(configFile)) + return; + + var backup = Path.Combine(dir, "HellionChat.json.pre-2.0.0.bak"); + if (File.Exists(backup)) + return; + + File.Copy(configFile, backup); + Log.Information($"HellionChat: config backed up to {backup} before the 2.0.0 reset"); + } + catch (Exception e) + { + Log.Warning(e, "HellionChat: could not back up the config before the 2.0.0 reset"); + } + } + private static void MigrateFromChatTwoLayout() { var pluginConfigsDir = Interface.ConfigDirectory.Parent?.FullName; @@ -956,7 +1027,7 @@ public sealed class Plugin : IAsyncDalamudPlugin { _hellionSettingsCmd = Commands.Register( "/hellion", - "Toggle Hellion Chat. /hellion settings opens settings, /hellion reset restores the default theme." + "Toggle Hellion Chat. /hellion settings opens settings, /hellion wizard reopens the setup wizard, /hellion reset restores the default theme." ); _hellionSettingsCmd.Execute += OnHellionSettingsCommand; @@ -1051,11 +1122,22 @@ public sealed class Plugin : IAsyncDalamudPlugin InputBarLab.Toggle(); return; } + + // The wizard had no way back into it: the reopen button the resource + // file still carries in 25 languages lost its call site in the + // four-step rewrite, and nothing replaced it. Without this the only + // route is closing the game and editing FirstRunCompleted by hand. + // Toggle, not IsOpen = true, so the same command closes it again. + if (arg.Equals("wizard", StringComparison.OrdinalIgnoreCase)) + { + FirstRunWizard.Toggle(); + return; + } #if DEBUG #endif if (arg.Equals("reset", StringComparison.OrdinalIgnoreCase)) { - // Recovery path documented in the v2.x master spec — drops a + // Documented recovery path -- drops a // broken custom theme out of the loader cache without touching // the user's JSON on disk. ThemeRegistry.SwitchSilent(Themes.ThemeRegistry.DefaultSlug); @@ -1170,7 +1252,7 @@ public sealed class Plugin : IAsyncDalamudPlugin // Schedule on the next framework tick to avoid the ~194ms // hitch from blocking with .Wait() while the frame finishes. // The Config.Tabs enumeration in ClearAllTabs/FilterAllTabs is - // now guarded by the shared Plugin.TabsListLock (B3), so this + // now guarded by the shared Plugin.TabsListLock, so this // tick scheduling is purely hitch-avoidance, not safety. // Pattern reference: SimpleTweaks // Tweaks/Chat/CaseInsensitiveCommands.cs:45. @@ -1257,7 +1339,7 @@ public sealed class Plugin : IAsyncDalamudPlugin private void Draw() { - // v1.9.0 B5: time the whole handler (style + font prologue included). + // v1.9.0: time the whole handler (style + font prologue included). // Bail before measuring once teardown has begun — a late Draw tick // must not touch ThemeRegistry / FontManager after DisposeAsync. if (_isDisposing) @@ -1266,7 +1348,7 @@ public sealed class Plugin : IAsyncDalamudPlugin var drawWatch = Stopwatch.StartNew(); try { - // v1.4.8 B2: pick up external edits of the active custom theme JSON + // v1.4.8: pick up external edits of the active custom theme JSON // without forcing the user to re-click the picker. The disk-stat is // 1Hz-throttled inside RefreshActiveIfStale, so this is essentially // free on built-in themes and ~1 stat/second on custom themes. @@ -1356,10 +1438,10 @@ public sealed class Plugin : IAsyncDalamudPlugin // Config.Tabs across the save so JSON includes them. Cloning only the // unpinned subset keeps the allocation proportional to // AutoTellTabsLimit (<=15) instead of the full tab list. - // B3: the strip/restore mutates the tab LIST, so it shares TabsListLock + // The strip/restore mutates the tab LIST, so it shares TabsListLock // with the worker add/remove and the refilter snapshot. Re-entrant: the // one worker caller (HandleTell) already holds it; framework callers take - // it here. SavePluginConfig runs inside (short, in-memory) — the §8 fallback + // it here. SavePluginConfig runs inside (short, in-memory) — the documented fallback // (serialize a copy outside the lock) is a tracked pre-beta to-do. lock (TabsListLock) { diff --git a/HellionChat/PluginHostFactory.cs b/HellionChat/PluginHostFactory.cs index c8fa192..6a860c3 100644 --- a/HellionChat/PluginHostFactory.cs +++ b/HellionChat/PluginHostFactory.cs @@ -15,7 +15,7 @@ namespace HellionChat; // Builds the generic-host DI container that drives v1.5.0+. The factory is // invoked synchronously from Plugin.ctor (after the schema gate clears) so the -// container exists before PluginLifecycle.LoadAsync runs. See plan §1 for the +// container exists before PluginLifecycle.LoadAsync runs. For the // deliberate divergence from Lightless' deferred Func-delegate pattern. internal static class PluginHostFactory { @@ -48,7 +48,7 @@ internal static class PluginHostFactory PluginHostDependencies dependencies ) { - // Block A — Dalamud services (21 [PluginService] singletons). + // Dalamud services (21 [PluginService] singletons). services.AddSingleton(dependencies); services.AddSingleton(dependencies.PluginInterface); services.AddSingleton(dependencies.PluginLog); @@ -77,7 +77,7 @@ internal static class PluginHostFactory services.AddSingleton(plugin.WindowSystem); services.AddSingleton(); - // Block B — HellionChat singletons. Factory lambdas because most + // HellionChat singletons. Factory lambdas because most // classes are internal-sealed and the default activator only sees // public ctors. services.AddSingleton(_ => new DalamudPlatformUtil()); @@ -307,7 +307,7 @@ internal static class PluginHostFactory // Pop-out windows: each gets its OWN MessageList + InputBar so the // channel pill and message scroll are per-window. The PayloadHandler is // attached post-build (ChannelPopoutInitHostedService), NEVER via ctor - // (plan §B.2 — would close a silent FactoryCallSite cycle). + // (would close a silent FactoryCallSite cycle). services.AddSingleton>(sp => slot => new Ui.Windows.ChannelPopoutWindow( slot, @@ -336,7 +336,7 @@ internal static class PluginHostFactory sp.GetRequiredService>() )); - // Block C — Windows. WindowSystem.AddWindow is called from + // Windows. WindowSystem.AddWindow is called from // PluginLifecycle.LoadAsync on the framework thread. services.AddSingleton(sp => new Ui.Windows.SettingsWindow( sp.GetRequiredService(), @@ -380,8 +380,8 @@ internal static class PluginHostFactory )); #endif // The style lab: variants side by side, in-game, against the live theme. - // Permanent by Flo's call, and deliberately not behind DEBUG -- style - // decisions happen in the build he actually runs. + // Permanent, and deliberately not behind DEBUG: style decisions get + // made in the build that actually ships. services.AddSingleton(sp => new Ui.Windows.InputBarLabWindow( sp.GetRequiredService() )); @@ -389,7 +389,10 @@ internal static class PluginHostFactory sp.GetRequiredService(), sp.GetRequiredService() )); - services.AddSingleton(sp => new FirstRunWizard(sp.GetRequiredService())); + services.AddSingleton(sp => new FirstRunWizard( + sp.GetRequiredService(), + sp.GetRequiredService() + )); // Hosted-service adapters: thin wrappers around the existing init // methods so the service class bodies stay unchanged. FontManager diff --git a/HellionChat/Privacy/PrivacyDefaults.cs b/HellionChat/Privacy/PrivacyDefaults.cs index 22f5c17..ff348c7 100644 --- a/HellionChat/Privacy/PrivacyDefaults.cs +++ b/HellionChat/Privacy/PrivacyDefaults.cs @@ -4,7 +4,7 @@ namespace HellionChat.Privacy; internal static class PrivacyDefaults { - // F3.1: failsafe for ChatTypes added by future FFXIV patches. New installs + // Failsafe for ChatTypes added by future FFXIV patches. New installs // persist unknown channels so a major patch's added ChatType isn't silently // dropped before the user can opt in or out. Existing configs keep their // explicit choice — see Configuration.cs PrivacyPersistUnknownChannels. diff --git a/HellionChat/Resources/HellionStrings.Designer.cs b/HellionChat/Resources/HellionStrings.Designer.cs index 9bbfa0c..218106b 100644 --- a/HellionChat/Resources/HellionStrings.Designer.cs +++ b/HellionChat/Resources/HellionStrings.Designer.cs @@ -116,10 +116,12 @@ internal class HellionStrings internal static string Wizard_Step1_Title => Get(nameof(Wizard_Step1_Title)); internal static string Wizard_Step1_Subtitle => Get(nameof(Wizard_Step1_Subtitle)); internal static string Wizard_Step1_Footer_Hint => Get(nameof(Wizard_Step1_Footer_Hint)); + internal static string Wizard_Step1_PluginNotice => Get(nameof(Wizard_Step1_PluginNotice)); + internal static string Wizard_Step1_Heritage => Get(nameof(Wizard_Step1_Heritage)); internal static string Wizard_Step1_Skip_Label => Get(nameof(Wizard_Step1_Skip_Label)); internal static string Wizard_Step1_Skip_Tooltip => Get(nameof(Wizard_Step1_Skip_Tooltip)); internal static string Wizard_Step2_Title => Get(nameof(Wizard_Step2_Title)); - internal static string Wizard_Step2_RecommendedFooter => Get(nameof(Wizard_Step2_RecommendedFooter)); + internal static string Wizard_Profile_Recommended_Badge => Get(nameof(Wizard_Profile_Recommended_Badge)); internal static string Wizard_Profile_Roleplay_Heading => Get(nameof(Wizard_Profile_Roleplay_Heading)); internal static string Wizard_Profile_Roleplay_Description => Get(nameof(Wizard_Profile_Roleplay_Description)); internal static string Wizard_Profile_Roleplay_Apply => Get(nameof(Wizard_Profile_Roleplay_Apply)); @@ -282,6 +284,7 @@ internal class HellionStrings // Hellion Chat — Default tab presets (channel-themed) internal static string Tabs_Presets_System => Get(nameof(Tabs_Presets_System)); + internal static string Tabs_Presets_Emote => Get(nameof(Tabs_Presets_Emote)); internal static string Tabs_Presets_FreeCompany => Get(nameof(Tabs_Presets_FreeCompany)); internal static string Tabs_Presets_Party => Get(nameof(Tabs_Presets_Party)); internal static string Tabs_Presets_Beginner => Get(nameof(Tabs_Presets_Beginner)); @@ -343,7 +346,7 @@ internal class HellionStrings // Hellion Chat — v1.2.1 Data Management tab section headings internal static string Settings_DataManagement_Advanced_Heading => Get(nameof(Settings_DataManagement_Advanced_Heading)); - // v1.5.6: Data & Privacy tab section titles (R6) + // v1.5.6: Data & Privacy tab section titles internal static string Settings_Section_PrivacyFilter => Get(nameof(Settings_Section_PrivacyFilter)); internal static string Settings_Section_Storage => Get(nameof(Settings_Section_Storage)); internal static string Settings_Section_Retention => Get(nameof(Settings_Section_Retention)); @@ -535,14 +538,14 @@ internal class HellionStrings internal static string Settings_General_CustomSoundVolume_Name => Get(nameof(Settings_General_CustomSoundVolume_Name)); internal static string Settings_General_CustomSoundVolume_Description => Get(nameof(Settings_General_CustomSoundVolume_Description)); - // v1.5.6: General tab collapsible section titles (R6) + // v1.5.6: General tab collapsible section titles internal static string Settings_Section_Input => Get(nameof(Settings_Section_Input)); internal static string Settings_Section_Sound => Get(nameof(Settings_Section_Sound)); internal static string Settings_Section_Language => Get(nameof(Settings_Section_Language)); internal static string Settings_Section_Performance => Get(nameof(Settings_Section_Performance)); internal static string Settings_Section_Sound_TabsHint => Get(nameof(Settings_Section_Sound_TabsHint)); - // v1.5.6: Chat tab collapsible section titles (R6) + // v1.5.6: Chat tab collapsible section titles internal static string Settings_Section_Messages => Get(nameof(Settings_Section_Messages)); internal static string Settings_Section_InputPreview => Get(nameof(Settings_Section_InputPreview)); internal static string Settings_Section_AutoTellTabs => Get(nameof(Settings_Section_AutoTellTabs)); @@ -550,7 +553,7 @@ internal class HellionStrings internal static string Settings_Section_LinksTooltips => Get(nameof(Settings_Section_LinksTooltips)); internal static string Settings_Section_NoviceNetwork => Get(nameof(Settings_Section_NoviceNetwork)); - // v1.5.6: Appearance tab collapsible section titles (R6) + // v1.5.6: Appearance tab collapsible section titles internal static string Settings_Section_Theme => Get(nameof(Settings_Section_Theme)); internal static string Settings_Section_Fonts => Get(nameof(Settings_Section_Fonts)); internal static string Settings_Section_Colours => Get(nameof(Settings_Section_Colours)); @@ -558,12 +561,12 @@ internal class HellionStrings internal static string Settings_Section_Timestamps => Get(nameof(Settings_Section_Timestamps)); internal static string Settings_Section_Animations => Get(nameof(Settings_Section_Animations)); - // v1.5.6: Window tab collapsible section titles (R6) + // v1.5.6: Window tab collapsible section titles internal static string Settings_Section_Hide => Get(nameof(Settings_Section_Hide)); internal static string Settings_Section_InactivityHide => Get(nameof(Settings_Section_InactivityHide)); internal static string Settings_Section_Frame => Get(nameof(Settings_Section_Frame)); - // v1.5.6: Tabs tab per-tab-item sub-section titles (R6) + // v1.5.6: Tabs tab per-tab-item sub-section titles internal static string Settings_Section_Tab_Channels => Get(nameof(Settings_Section_Tab_Channels)); internal static string Settings_Section_Tab_Display => Get(nameof(Settings_Section_Tab_Display)); internal static string Settings_Section_Tab_Notification => Get(nameof(Settings_Section_Tab_Notification)); @@ -571,7 +574,7 @@ internal class HellionStrings internal static string Settings_Section_Tab_PopOut => Get(nameof(Settings_Section_Tab_PopOut)); internal static string Settings_Section_Tab_Volume_AllTabsHint => Get(nameof(Settings_Section_Tab_Volume_AllTabsHint)); - // v1.5.6: About tab collapsible section titles (R6) + // v1.5.6: About tab collapsible section titles internal static string Settings_Section_Extensions => Get(nameof(Settings_Section_Extensions)); internal static string Settings_Section_PluginInfo => Get(nameof(Settings_Section_PluginInfo)); internal static string Settings_Section_Project => Get(nameof(Settings_Section_Project)); diff --git a/HellionChat/Resources/HellionStrings.ca.resx b/HellionChat/Resources/HellionStrings.ca.resx index c843d03..fed51c2 100644 --- a/HellionChat/Resources/HellionStrings.ca.resx +++ b/HellionChat/Resources/HellionStrings.ca.resx @@ -172,7 +172,7 @@ Tria un perfil inicial. Podràs ajustar-ho tot més tard a Configuració → Privadesa. - Minimització de dades (recomanat) + Minimització de dades Només es guarden les teves pròpies converses: tells, party, FC, linkshells, cross-world linkshells, alliance i ExtraChat. El xat públic, els diàlegs dels PNJ i el correu brossa del sistema es descarten al nivell d'emmagatzematge. La retenció segueix els valors per defecte de l'especificació (tells 365 dies, canals de conversa propis 90 dies). @@ -214,7 +214,13 @@ Benvingut a Hellion Chat - Un fork de Chat 2 de Hellion Forge amb valors per defecte respectuosos amb la privadesa, visuals coherents amb la marca i alguns retocs de qualitat de vida. + La teva finestra de xat, de Hellion Forge. Privadesa des del primer moment, en 25 idiomes, i l'organitzes com vulguis. + + + Hellion Chat va començar com un fork de Chat 2. Des d'aleshores tots dos s'han allunyat prou perquè les bases de codi ja no siguin compatibles. + + + Els plugins són una zona grisa a Final Fantasy XIV: les condicions d'ús de Square Enix no els cobreixen, i Naoki Yoshida ha demanat públicament que no se'n faci publicitat. Mantén, doncs, el tema fora de Say, Yell, Shout i de qualsevol altre canal públic. Tres passos ràpids. Podràs canviar-ho tot més tard a Configuració → Hellion Chat. @@ -228,8 +234,8 @@ Què es guarda? - - ★ = recomanat per a la majoria de jugadors. + + Recomanat Roleplay @@ -298,7 +304,7 @@ (sense canvis) - 💡 Prova-ho: escriu /tell <Nom del jugador> al xat. Hellion Chat obre una pestanya dedicada per a la conversa i precarrega els últims {0} missatges. + Prova-ho: escriu /tell <Nom del jugador> al xat. Hellion Chat obre una pestanya dedicada per a la conversa i precarrega els últims {0} missatges. Configuració → Hellion Chat per ajustar-ho més tard @@ -605,6 +611,9 @@ Sistema + + Emotes + Free Company diff --git a/HellionChat/Resources/HellionStrings.cs.resx b/HellionChat/Resources/HellionStrings.cs.resx index 048653e..783b971 100644 --- a/HellionChat/Resources/HellionStrings.cs.resx +++ b/HellionChat/Resources/HellionStrings.cs.resx @@ -172,7 +172,7 @@ Vyber si výchozí profil. Vše můžeš kdykoli později upravit v Nastavení → Soukromí. - Minimalizace dat (doporučeno) + Minimalizace dat Ukládají se pouze tvoje vlastní konverzace: telly, party, FC, linkshelly, cross-world linkshelly, aliance a ExtraChat. Veřejný chat, dialogy NPC a systémový spam se na úrovni úložiště zahodí. Uchovávání dle výchozích hodnot specifikace (telly 365 dní, vlastní konverzační kanály 90 dní). @@ -214,7 +214,13 @@ Vítej v Hellion Chat - Fork Chat 2 od Hellion Forge s výchozím nastavením ohleduplným ke soukromí, vizuálem odpovídajícím značce a pár vylepšeními kvality života. + Tvoje okno chatu od Hellion Forge. Soukromí od prvního spuštění, 25 jazyků a rozvržení si nastavíš sám. + + + Hellion Chat vznikl jako fork Chat 2. Od té doby se oba projekty rozešly natolik, že jejich kódové základny už nejsou kompatibilní. + + + Pluginy jsou ve Final Fantasy XIV šedá zóna: podmínky užívání Square Enix je nepokrývají a Naoki Yoshida veřejně požádal, aby se nepropagovaly. Nezmiňuj proto toto téma na Say, Yell, Shout ani na žádném jiném veřejném kanálu. Tři krátké kroky. Vše můžeš kdykoli změnit v Nastavení → Hellion Chat. @@ -228,8 +234,8 @@ Co se bude ukládat? - - ★ = doporučeno pro většinu hráčů. + + Doporučeno Roleplay @@ -298,7 +304,7 @@ (beze změny) - 💡 Vyzkoušej: napiš /tell <Jméno hráče> do chatu. Hellion Chat automaticky otevře vlastní záložku pro konverzaci a přednačte posledních {0} zpráv. + Vyzkoušej: napiš /tell <Jméno hráče> do chatu. Hellion Chat automaticky otevře vlastní záložku pro konverzaci a přednačte posledních {0} zpráv. Nastavení → Hellion Chat pro pozdější doladění @@ -605,6 +611,9 @@ Systém + + Emoty + Free Company diff --git a/HellionChat/Resources/HellionStrings.da.resx b/HellionChat/Resources/HellionStrings.da.resx index 2c9743c..5f7e648 100644 --- a/HellionChat/Resources/HellionStrings.da.resx +++ b/HellionChat/Resources/HellionStrings.da.resx @@ -172,7 +172,7 @@ Vælg en startprofil. Du kan justere alt efterfølgende under Indstillinger → Privatliv. - Dataminimering (anbefalet) + Dataminimering Kun dine egne samtaler gemmes: tells, gruppe, FC, linkshells, cross-world linkshells, alliance og ExtraChat. Offentlig chat, NPC-dialoger og systemstøj kasseres på lagerniveau. Opbevaring følger spec-standarder (tells 365 dage, egne samtalekanaler 90 dage). @@ -214,7 +214,13 @@ Velkommen til Hellion Chat - En Chat 2 fork fra Hellion Forge med privatlivsbevidste standarder, brandkonsistent udseende og et par praktiske forbedringer. + Dit chatvindue fra Hellion Forge. Privatliv fra første start, 25 sprog, og du sætter det op, som du vil. + + + Hellion Chat startede som en fork af Chat 2. De to har siden fjernet sig så meget fra hinanden, at kodebaserne ikke længere er kompatible. + + + Plugins er en gråzone i Final Fantasy XIV: Square Enix' brugsvilkår dækker dem ikke, og Naoki Yoshida har offentligt bedt om, at man ikke reklamerer for dem. Hold derfor emnet ude af Say, Yell, Shout og alle andre offentlige kanaler. Tre korte trin. Du kan ændre alt efterfølgende under Indstillinger → Hellion Chat. @@ -228,8 +234,8 @@ Hvad gemmes? - - ★ = anbefalet til de fleste spillere. + + Anbefalet Roleplay @@ -298,7 +304,7 @@ (uændret) - 💡 Prøv det: skriv /tell <Spillernavn> i chatten. Hellion Chat åbner en dedikeret tab til samtalen og forudindlæser de sidste {0} beskeder. + Prøv det: skriv /tell <Spillernavn> i chatten. Hellion Chat åbner en dedikeret tab til samtalen og forudindlæser de sidste {0} beskeder. Indstillinger → Hellion Chat for at finjustere senere @@ -605,6 +611,9 @@ System + + Emotes + Free Company diff --git a/HellionChat/Resources/HellionStrings.de.resx b/HellionChat/Resources/HellionStrings.de.resx index 0ebcdee..8484335 100644 --- a/HellionChat/Resources/HellionStrings.de.resx +++ b/HellionChat/Resources/HellionStrings.de.resx @@ -172,7 +172,7 @@ Wähle ein Start-Profil. Du kannst später alles unter Einstellungen → Datenschutz anpassen. - Datensparsamkeit (empfohlen) + Datensparsamkeit Es werden nur deine eigenen Konversationen gespeichert: Flüsternachrichten, Gruppe, FC, Linkshells, Cross-World-Linkshells, Allianz und ExtraChat. Öffentlicher Chat, NPC-Dialoge und System-Spam werden auf der Storage-Ebene verworfen. Aufbewahrung nach Spec-Defaults (Flüsternachrichten 365 Tage, eigene Konversations-Kanäle 90 Tage). @@ -214,7 +214,13 @@ Willkommen bei Hellion Chat - Ein Chat 2 Fork von Hellion Forge mit DSGVO-konformen Defaults, brand-konsistentem Look und Quality-of-Life-Verbesserungen. + Dein Chat-Fenster von Hellion Forge. Datensparsam ab Werk, in 25 Sprachen, und du richtest es dir ein wie du willst. + + + Plugins sind in Final Fantasy XIV eine Grauzone: Die Nutzungsbedingungen von Square Enix decken sie nicht ab, und Naoki Yoshida hat öffentlich darum gebeten, nicht damit zu werben. Halte das Thema deshalb aus Sagen, Rufen, Schreien und allen anderen öffentlichen Kanälen heraus. + + + Hellion Chat ging ursprünglich aus Chat 2 hervor. Beide haben sich seitdem so weit auseinanderentwickelt, dass die Codebasen nicht mehr kompatibel sind. 3 kurze Schritte. Du kannst alles später unter Einstellungen → Hellion Chat ändern. @@ -228,8 +234,8 @@ Was darf gespeichert werden? - - ★ = empfohlen für die meisten Spieler. + + Empfohlen Roleplay @@ -298,7 +304,7 @@ (unverändert) - 💡 Probier's aus: Tipp /tell <Spielername> in den Chat. Hellion Chat öffnet automatisch einen eigenen Tab für die Unterhaltung und lädt die letzten {0} Messages mit. + Probier's aus: Tipp /tell <Spielername> in den Chat. Hellion Chat öffnet automatisch einen eigenen Tab für die Unterhaltung und lädt die letzten {0} Messages mit. Einstellungen → Hellion Chat zum späteren Anpassen @@ -605,6 +611,9 @@ System + + Emotes + Free Company diff --git a/HellionChat/Resources/HellionStrings.el.resx b/HellionChat/Resources/HellionStrings.el.resx index 33afe56..294ca5e 100644 --- a/HellionChat/Resources/HellionStrings.el.resx +++ b/HellionChat/Resources/HellionStrings.el.resx @@ -172,7 +172,7 @@ Επίλεξε ένα αρχικό προφίλ. Μπορείς να ρυθμίσεις τα πάντα αργότερα στις Ρυθμίσεις → Απόρρητο. - Ελαχιστοποίηση δεδομένων (συνιστάται) + Ελαχιστοποίηση δεδομένων Αποθηκεύονται μόνο οι δικές σου συνομιλίες: tells, party, FC, linkshells, cross-world linkshells, alliance και ExtraChat. Το δημόσιο chat, οι διάλογοι NPC και το system spam απορρίπτονται σε επίπεδο αποθήκευσης. Η διατήρηση ακολουθεί τις προεπιλογές spec (tells 365 ημέρες, κανάλια ιδιωτικών συνομιλιών 90 ημέρες). @@ -214,7 +214,13 @@ Καλωσόρισες στο Hellion Chat - Ένα fork του Chat 2 από το Hellion Forge με προεπιλογές φιλικές στο απόρρητο, συνεπή εμφάνιση brand και μερικές βελτιώσεις ευχρηστίας. + Το παράθυρο συνομιλίας σου από το Hellion Forge. Ιδιωτικότητα από την αρχή, σε 25 γλώσσες, και το στήνεις όπως θέλεις. + + + Το Hellion Chat ξεκίνησε ως fork του Chat 2. Έκτοτε τα δύο έχουν απομακρυνθεί τόσο ώστε οι κώδικές τους να μην είναι πλέον συμβατοί. + + + Τα plugin αποτελούν γκρίζα ζώνη στο Final Fantasy XIV: οι όροι χρήσης της Square Enix δεν τα καλύπτουν και ο Naoki Yoshida έχει ζητήσει δημόσια να μην διαφημίζονται. Κράτα λοιπόν το θέμα μακριά από τα Say, Yell, Shout και κάθε άλλο δημόσιο κανάλι. Τρία σύντομα βήματα. Μπορείς να αλλάξεις τα πάντα αργότερα στις Ρυθμίσεις → Hellion Chat. @@ -228,8 +234,8 @@ Τι αποθηκεύεται; - - ★ = συνιστάται για τους περισσότερους παίκτες. + + Συνιστάται Roleplay @@ -298,7 +304,7 @@ (αμετάβλητο) - 💡 Δοκίμασέ το: πληκτρολόγησε /tell <Όνομα Παίκτη> στο chat. Το Hellion Chat ανοίγει αυτόματα μια αποκλειστική καρτέλα για τη συνομιλία και προφορτώνει τα τελευταία {0} μηνύματα. + Δοκίμασέ το: πληκτρολόγησε /tell <Όνομα Παίκτη> στο chat. Το Hellion Chat ανοίγει αυτόματα μια αποκλειστική καρτέλα για τη συνομιλία και προφορτώνει τα τελευταία {0} μηνύματα. Ρυθμίσεις → Hellion Chat για προσαρμογή αργότερα @@ -605,6 +611,9 @@ System + + Emotes + Free Company diff --git a/HellionChat/Resources/HellionStrings.es.resx b/HellionChat/Resources/HellionStrings.es.resx index 81d99d8..b60515f 100644 --- a/HellionChat/Resources/HellionStrings.es.resx +++ b/HellionChat/Resources/HellionStrings.es.resx @@ -172,7 +172,7 @@ Elige un perfil inicial. Puedes ajustarlo todo más tarde en Ajustes → Privacidad. - Minimización de datos (recomendado) + Minimización de datos Solo se almacenan tus propias conversaciones: tells, escuadrón, FC, linkshells, linkshells cross-world, alianza y ExtraChat. El chat público, los diálogos de PNJ y el spam del sistema se descartan a nivel de almacenamiento. La retención sigue los valores predeterminados de spec (tells 365 días, canales de conversación propios 90 días). @@ -214,7 +214,13 @@ Bienvenido a Hellion Chat - Un fork de Chat 2 de Hellion Forge con valores predeterminados respetuosos con la privacidad, diseño coherente con la marca y algunas mejoras de calidad de vida. + Tu ventana de chat, de Hellion Forge. Privacidad desde el primer momento, traducida a 25 idiomas, y la organizas a tu gusto. + + + Hellion Chat nació como un fork de Chat 2. Desde entonces ambos se han separado lo suficiente como para que sus bases de código ya no sean compatibles. + + + Los plugins son una zona gris en Final Fantasy XIV: las condiciones de uso de Square Enix no los contemplan, y Naoki Yoshida ha pedido públicamente que no se promocionen. Mantén el tema fuera de Decir, Gritar, Vociferar y de cualquier otro canal público. Tres pasos breves. Puedes cambiar todo más tarde en Ajustes → Hellion Chat. @@ -228,8 +234,8 @@ ¿Qué se almacena? - - ★ = recomendado para la mayoría de jugadores. + + Recomendado Roleplay @@ -298,7 +304,7 @@ (sin cambios) - 💡 Pruébalo: escribe /tell <Nombre del jugador> en el chat. Hellion Chat abre una pestaña dedicada para la conversación y precarga los últimos {0} mensajes. + Pruébalo: escribe /tell <Nombre del jugador> en el chat. Hellion Chat abre una pestaña dedicada para la conversación y precarga los últimos {0} mensajes. Ajustes → Hellion Chat para personalizar más tarde @@ -605,6 +611,9 @@ Sistema + + Emotes + Free Company diff --git a/HellionChat/Resources/HellionStrings.fi.resx b/HellionChat/Resources/HellionStrings.fi.resx index e9641cf..2de9366 100644 --- a/HellionChat/Resources/HellionStrings.fi.resx +++ b/HellionChat/Resources/HellionStrings.fi.resx @@ -172,7 +172,7 @@ Valitse aloitusprofiili. Voit muuttaa kaikkea myöhemmin kohdassa Asetukset → Tietosuoja. - Tietojen minimointi (suositeltu) + Tietojen minimointi Vain omat keskustelusi tallennetaan: tellit, ryhmä, FC, linkshells, cross-world linkshells, allianssi ja ExtraChat. Julkinen chat, NPC-dialogit ja järjestelmäroskaviestit hylätään tallennusvaiheessa. Säilytys noudattaa spec-oletuksia (tellit 365 päivää, omat keskustelukanavat 90 päivää). @@ -214,7 +214,13 @@ Tervetuloa Hellion Chatiin - Chat 2 -haarautuma Hellion Forgelta, tietosuojatietoisilla oletuksilla, yhtenäisellä visuaalisella ilmeellä ja muutamilla käytännöllisillä parannuksilla. + Chat-ikkunasi Hellion Forgelta. Yksityisyys heti alusta, 25 kieltä, ja järjestät sen kuten haluat. + + + Hellion Chat sai alkunsa Chat 2:n forkkina. Sittemmin ne ovat eronneet toisistaan niin paljon, etteivät koodipohjat ole enää yhteensopivia. + + + Pluginit ovat Final Fantasy XIV:ssä harmaata aluetta: Square Enixin käyttöehdot eivät kata niitä, ja Naoki Yoshida on julkisesti pyytänyt, ettei niitä mainostettaisi. Pidä aihe siis poissa kanavilta Say, Yell, Shout ja kaikilta muilta julkisilta kanavilta. Kolme lyhyttä vaihetta. Voit muuttaa kaikkea myöhemmin kohdassa Asetukset → Hellion Chat. @@ -228,8 +234,8 @@ Mitä tallennetaan? - - ★ = suositeltu useimmille pelaajille. + + Suositeltu Roleplay @@ -298,7 +304,7 @@ (ei muutosta) - 💡 Kokeile: kirjoita /tell <Pelaajan nimi> chattiin. Hellion Chat avaa erillisen välilehden keskustelulle ja esivalmistelee viimeiset {0} viestiä. + Kokeile: kirjoita /tell <Pelaajan nimi> chattiin. Hellion Chat avaa erillisen välilehden keskustelulle ja esivalmistelee viimeiset {0} viestiä. Asetukset → Hellion Chat hienosäätöä varten myöhemmin @@ -605,6 +611,9 @@ Järjestelmä + + Emootiot + Free Company diff --git a/HellionChat/Resources/HellionStrings.fr.resx b/HellionChat/Resources/HellionStrings.fr.resx index a1baa48..7a58f98 100644 --- a/HellionChat/Resources/HellionStrings.fr.resx +++ b/HellionChat/Resources/HellionStrings.fr.resx @@ -172,7 +172,7 @@ Choisissez un profil de départ. Vous pouvez tout ajuster par la suite dans Paramètres → Confidentialité. - Minimisation des données (recommandé) + Minimisation des données Seules vos propres conversations sont enregistrées : messages privés, équipe, CL, linkshells, linkshells inter-mondes, alliance et ExtraChat. Le chat public, les dialogues PNJ et le spam système sont écartés au niveau du stockage. La conservation suit les valeurs par défaut de la spécification (messages privés 365 jours, vos canaux de conversation 90 jours). @@ -214,7 +214,13 @@ Bienvenue dans Hellion Chat - Un fork de Chat 2 par Hellion Forge avec des valeurs par défaut axées sur la confidentialité, une identité visuelle cohérente et quelques améliorations de confort. + Ta fenêtre de discussion, signée Hellion Forge. Confidentialité par défaut, traduite en 25 langues, et à agencer comme tu veux. + + + Hellion Chat est né d'un fork de Chat 2. Les deux se sont depuis suffisamment éloignés pour que les bases de code ne soient plus compatibles. + + + Les plugins sont une zone grise dans Final Fantasy XIV : les conditions d'utilisation de Square Enix ne les couvrent pas, et Naoki Yoshida a publiquement demandé de ne pas en faire la promotion. Évite donc le sujet dans Dire, Crier, Hurler et tout autre canal public. Trois étapes courtes. Vous pouvez tout modifier plus tard dans Paramètres → Hellion Chat. @@ -228,8 +234,8 @@ Qu'est-ce qui est enregistré ? - - ★ = recommandé pour la plupart des joueurs. + + Recommandé Roleplay @@ -298,7 +304,7 @@ (inchangé) - 💡 Essayez : tapez /tell <Nom du joueur> dans le chat. Hellion Chat ouvre un onglet dédié à la conversation et précharge les {0} derniers messages. + Essayez : tapez /tell <Nom du joueur> dans le chat. Hellion Chat ouvre un onglet dédié à la conversation et précharge les {0} derniers messages. Paramètres → Hellion Chat pour affiner plus tard @@ -605,6 +611,9 @@ Système + + Emotes + Compagnie libre diff --git a/HellionChat/Resources/HellionStrings.hu.resx b/HellionChat/Resources/HellionStrings.hu.resx index f00d6b3..90cdf6c 100644 --- a/HellionChat/Resources/HellionStrings.hu.resx +++ b/HellionChat/Resources/HellionStrings.hu.resx @@ -172,7 +172,7 @@ Válassz egy kezdő profilt. Mindent később is módosíthatsz a Beállítások → Adatvédelem menüpontban. - Adatminimalizálás (ajánlott) + Adatminimalizálás Csak a saját beszélgetéseid tárolódnak: tellek, party, FC, linkshellек, cross-world linkshellек, szövetség és ExtraChat. A nyilvános chat, az NPC-párbeszédek és a rendszerüzenetek a tárolás szintjén eldobódnak. A megőrzés a spec-alapértelmezettet követi (tellek 365 nap, saját csatornák 90 nap). @@ -214,7 +214,13 @@ Üdvözöl a Hellion Chat - Egy Chat 2 fork a Hellion Forge-tól, adatvédelmet szem előtt tartó alapértelmezésekkel, egységes arculattal és néhány kényelmi funkcióval. + A te chatablakod a Hellion Forge-tól. Adatvédelem az első indítástól, 25 nyelven, és úgy rendezed be, ahogy szeretnéd. + + + A Hellion Chat a Chat 2 forkjaként indult. A kettő azóta annyira eltávolodott egymástól, hogy a kódbázisok már nem kompatibilisek. + + + A pluginek a Final Fantasy XIV-ben szürke zónát jelentenek: a Square Enix felhasználási feltételei nem terjednek ki rájuk, és Naoki Yoshida nyilvánosan kérte, hogy ne reklámozzák őket. Ne hozd tehát szóba a témát a Say, Yell, Shout és bármely más nyilvános csatornán. Három rövid lépés. Mindent megváltoztathatsz később a Beállítások → Hellion Chat menüpontban. @@ -228,8 +234,8 @@ Mi tárolódjon? - - ★ = a legtöbb játékosnak ajánlott. + + Ajánlott Roleplay @@ -298,7 +304,7 @@ (változatlan) - 💡 Próbáld ki: írj /tell <Játékosnév> a chatbe. A Hellion Chat automatikusan megnyit egy külön fület a beszélgetéshez, és előtölti az utolsó {0} üzenetet. + Próbáld ki: írj /tell <Játékosnév> a chatbe. A Hellion Chat automatikusan megnyit egy külön fület a beszélgetéshez, és előtölti az utolsó {0} üzenetet. Beállítások → Hellion Chat a finomhangoláshoz @@ -605,6 +611,9 @@ Rendszer + + Emote-ok + Free Company diff --git a/HellionChat/Resources/HellionStrings.it.resx b/HellionChat/Resources/HellionStrings.it.resx index ef3f54e..c823dc8 100644 --- a/HellionChat/Resources/HellionStrings.it.resx +++ b/HellionChat/Resources/HellionStrings.it.resx @@ -172,7 +172,7 @@ Scegli un profilo di partenza. Puoi regolare tutto in seguito in Impostazioni → Privacy. - Minimizzazione dei dati (consigliata) + Minimizzazione dei dati Vengono salvate solo le tue conversazioni: tell, party, FC, linkshell, cross-world linkshell, alliance ed ExtraChat. La chat pubblica, i dialoghi NPC e lo spam di sistema vengono scartati a livello di archiviazione. La conservazione segue i valori predefiniti dello spec (tell 365 giorni, canali di conversazione propri 90 giorni). @@ -214,7 +214,13 @@ Benvenuto in Hellion Chat - Un fork di Chat 2 da Hellion Forge con impostazioni predefinite attente alla privacy, un'estetica coerente con il brand e qualche miglioramento alla qualità della vita. + La tua finestra di chat, firmata Hellion Forge. Privacy fin da subito, tradotta in 25 lingue, e la sistemi come vuoi. + + + Hellion Chat è nato come fork di Chat 2. Da allora i due si sono allontanati al punto che le basi di codice non sono più compatibili. + + + I plugin sono una zona grigia in Final Fantasy XIV: le condizioni d'uso di Square Enix non li contemplano, e Naoki Yoshida ha chiesto pubblicamente di non pubblicizzarli. Tieni quindi l'argomento fuori da Say, Yell, Shout e da ogni altro canale pubblico. Tre brevi passaggi. Puoi cambiare tutto in seguito in Impostazioni → Hellion Chat. @@ -228,8 +234,8 @@ Cosa viene salvato? - - ★ = consigliato per la maggior parte dei giocatori. + + Consigliato Roleplay @@ -298,7 +304,7 @@ (invariato) - 💡 Provalo: digita /tell <Nome Giocatore> in chat. Hellion Chat apre un tab dedicato alla conversazione e precarica gli ultimi {0} messaggi. + Provalo: digita /tell <Nome Giocatore> in chat. Hellion Chat apre un tab dedicato alla conversazione e precarica gli ultimi {0} messaggi. Impostazioni → Hellion Chat per regolazioni successive @@ -605,6 +611,9 @@ Sistema + + Emote + Free Company diff --git a/HellionChat/Resources/HellionStrings.ja.resx b/HellionChat/Resources/HellionStrings.ja.resx index 9d700ef..e27e937 100644 --- a/HellionChat/Resources/HellionStrings.ja.resx +++ b/HellionChat/Resources/HellionStrings.ja.resx @@ -172,7 +172,7 @@ 開始プロファイルを選択してください。後で設定 → プライバシーからいつでも変更できます。 - データ最小化(推奨) + データ最小化 自分の会話のみ保存されます: テル、パーティ、フリーカンパニー、リンクシェル、クロスワールドリンクシェル、アライアンス、ExtraChat。パブリックチャット、NPCの台詞、システムスパムはストレージレベルで破棄されます。保持期間は仕様デフォルト(テル 365日、自分の会話チャンネル 90日)に従います。 @@ -214,7 +214,13 @@ Hellion Chat へようこそ - Hellion Forge による Chat 2 フォーク。プライバシーに配慮したデフォルト設定、ブランド一貫のビジュアル、そしていくつかの利便性向上機能を備えています。 + Hellion Forge がお届けするチャットウィンドウ。初期設定からプライバシー重視、25言語対応、レイアウトは自由に組み替えられます。 + + + Hellion Chat は Chat 2 のフォークとして始まりました。その後、両者は大きく分かれ、コードベースはすでに互換性がありません。 + + + ファイナルファンタジーXIVにおいてプラグインはグレーゾーンです。スクウェア・エニックスの利用規約は対象としておらず、吉田直樹氏も公の場で宣伝しないよう求めています。Say、Yell、Shout をはじめとする公開チャンネルでは話題にしないでください。 3つの短いステップです。後で設定 → Hellion Chat からすべて変更できます。 @@ -228,8 +234,8 @@ 何が保存されますか? - - ★ = ほとんどのプレイヤーに推奨。 + + 推奨 ロールプレイ @@ -298,7 +304,7 @@ (変更なし) - 💡 試してみましょう: チャットに /tell <プレイヤー名> と入力してください。Hellion Chat が会話専用のタブを自動で開き、最新 {0} 件のメッセージをプリロードします。 + 試してみましょう: チャットに /tell <プレイヤー名> と入力してください。Hellion Chat が会話専用のタブを自動で開き、最新 {0} 件のメッセージをプリロードします。 設定 → Hellion Chat で後から細かく調整できます @@ -605,6 +611,9 @@ システム + + エモート + フリーカンパニー diff --git a/HellionChat/Resources/HellionStrings.ko.resx b/HellionChat/Resources/HellionStrings.ko.resx index eb431f0..0d4866f 100644 --- a/HellionChat/Resources/HellionStrings.ko.resx +++ b/HellionChat/Resources/HellionStrings.ko.resx @@ -172,7 +172,7 @@ 시작 프로필을 선택하세요. 나중에 설정 → 개인정보에서 모두 조정할 수 있습니다. - 데이터 최소화 (권장) + 데이터 최소화 자신의 대화만 저장됩니다. 귓속말, 파티, 자유부대, 링크셸, 서버 초월 링크셸, 연합 파티, ExtraChat이 포함됩니다. 공개 채팅, NPC 대화, 시스템 스팸은 저장 단계에서 제외됩니다. 보존 기간은 기본 스펙을 따릅니다 (귓속말 365일, 개인 대화 채널 90일). @@ -214,7 +214,13 @@ Hellion Chat에 오신 것을 환영합니다 - Hellion Forge에서 만든 Chat 2 포크입니다. 개인정보 보호 기본값, 브랜드 일관성 있는 디자인, 그리고 몇 가지 편의 기능을 제공합니다. + Hellion Forge가 만든 채팅 창입니다. 기본값부터 개인정보 우선, 25개 언어 지원, 배치는 원하는 대로. + + + Hellion Chat은 Chat 2의 포크로 시작했습니다. 그 뒤로 둘은 코드베이스가 더 이상 호환되지 않을 만큼 멀어졌습니다. + + + 파이널 판타지 XIV에서 플러그인은 회색 지대입니다. 스퀘어 에닉스 이용약관은 이를 다루지 않으며, 요시다 나오키는 공개적으로 홍보하지 말아 달라고 요청했습니다. 말하기, 떠들기, 외치기를 비롯한 모든 공개 채널에서는 이 주제를 꺼내지 마세요. 세 가지 간단한 단계입니다. 나중에 설정 → Hellion Chat에서 모두 변경할 수 있습니다. @@ -228,8 +234,8 @@ 무엇을 저장할까요? - - ★ = 대부분의 플레이어에게 권장됩니다. + + 권장 롤플레이 @@ -298,7 +304,7 @@ (변경 없음) - 💡 테스트해보세요. 채팅창에 /tell <Player Name>을 입력하면 Hellion Chat이 대화를 위한 전용 탭을 열고 마지막 {0}개의 메시지를 미리 불러옵니다. + 테스트해보세요. 채팅창에 /tell <Player Name>을 입력하면 Hellion Chat이 대화를 위한 전용 탭을 열고 마지막 {0}개의 메시지를 미리 불러옵니다. 나중에 세부 조정은 설정 → Hellion Chat에서 @@ -605,6 +611,9 @@ 시스템 + + 감정 표현 + 자유부대 diff --git a/HellionChat/Resources/HellionStrings.nb.resx b/HellionChat/Resources/HellionStrings.nb.resx index 0c5073e..3f3e4ea 100644 --- a/HellionChat/Resources/HellionStrings.nb.resx +++ b/HellionChat/Resources/HellionStrings.nb.resx @@ -172,7 +172,7 @@ Velg en startprofil. Du kan justere alt senere under Innstillinger → Personvern. - Dataminimering (anbefalt) + Dataminimering Bare dine egne samtaler lagres: tells, party, FC, linkshells, cross-world linkshells, alliance og ExtraChat. Offentlig chat, NPC-dialoger og systemspam forkastes på lagringsnivå. Oppbevaring følger spec-standarder (tells 365 dager, egne samtalekanaler 90 dager). @@ -214,7 +214,13 @@ Velkommen til Hellion Chat - En Chat 2-fork fra Hellion Forge med personvernvennlige standarder, merkevarekonsekvente visuals og noen livskvalitetsforbedringer. + Chatvinduet ditt fra Hellion Forge. Personvern fra første start, 25 språk, og du setter det opp slik du vil. + + + Hellion Chat startet som en fork av Chat 2. De to har siden fjernet seg så mye fra hverandre at kodebasene ikke lenger er kompatible. + + + Plugins er en gråsone i Final Fantasy XIV: Square Enix' bruksvilkår dekker dem ikke, og Naoki Yoshida har offentlig bedt om at man ikke reklamerer for dem. Hold derfor temaet unna Say, Yell, Shout og alle andre offentlige kanaler. Tre korte steg. Du kan endre alt senere under Innstillinger → Hellion Chat. @@ -228,8 +234,8 @@ Hva blir lagret? - - ★ = anbefalt for de fleste spillere. + + Anbefalt Roleplay @@ -298,7 +304,7 @@ (uendret) - 💡 Prøv det: skriv /tell <Spillernavn> i chatten. Hellion Chat åpner en dedikert fane for samtalen og forhåndslaster de siste {0} meldingene. + Prøv det: skriv /tell <Spillernavn> i chatten. Hellion Chat åpner en dedikert fane for samtalen og forhåndslaster de siste {0} meldingene. Innstillinger → Hellion Chat for å finjustere senere @@ -605,6 +611,9 @@ System + + Emotes + Free Company diff --git a/HellionChat/Resources/HellionStrings.nl.resx b/HellionChat/Resources/HellionStrings.nl.resx index a44f9b3..52178d7 100644 --- a/HellionChat/Resources/HellionStrings.nl.resx +++ b/HellionChat/Resources/HellionStrings.nl.resx @@ -172,7 +172,7 @@ Kies een startprofiel. Je kunt later alles aanpassen via Instellingen → Privacy. - Dataminimalisatie (aanbevolen) + Dataminimalisatie Alleen je eigen gesprekken worden opgeslagen: tells, groep, FC, linkshells, cross-world linkshells, alliantie en ExtraChat. Openbare chat, NPC-dialogen en systeemspam worden op opslagniveau verwijderd. Retentie volgt spec-standaarden (tells 365 dagen, eigen gesprekkanalen 90 dagen). @@ -214,7 +214,13 @@ Welkom bij Hellion Chat - Een Chat 2 fork van Hellion Forge met privacybewuste standaarden, merkconforme uitstraling en handige quality-of-life verbeteringen. + Jouw chatvenster van Hellion Forge. Privacy vanaf de eerste start, in 25 talen, en je richt het in zoals jij wilt. + + + Hellion Chat begon als een fork van Chat 2. De twee zijn sindsdien zo ver uit elkaar gegroeid dat de codebases niet meer compatibel zijn. + + + Plugins zijn in Final Fantasy XIV een grijs gebied: de gebruiksvoorwaarden van Square Enix dekken ze niet, en Naoki Yoshida heeft publiekelijk gevraagd er geen reclame voor te maken. Houd het onderwerp dus buiten Zeg, Roep, Schreeuw en elk ander openbaar kanaal. Drie korte stappen. Je kunt later alles aanpassen via Instellingen → Hellion Chat. @@ -228,8 +234,8 @@ Wat wordt er opgeslagen? - - ★ = aanbevolen voor de meeste spelers. + + Aanbevolen Roleplay @@ -298,7 +304,7 @@ (ongewijzigd) - 💡 Probeer het uit: typ /tell <Spelernaam> in de chat. Hellion Chat opent een eigen tabblad voor het gesprek en laadt de laatste {0} berichten vooraf. + Probeer het uit: typ /tell <Spelernaam> in de chat. Hellion Chat opent een eigen tabblad voor het gesprek en laadt de laatste {0} berichten vooraf. Instellingen → Hellion Chat om later te verfijnen @@ -605,6 +611,9 @@ Systeem + + Emotes + Free Company diff --git a/HellionChat/Resources/HellionStrings.pl.resx b/HellionChat/Resources/HellionStrings.pl.resx index 567421d..a42094c 100644 --- a/HellionChat/Resources/HellionStrings.pl.resx +++ b/HellionChat/Resources/HellionStrings.pl.resx @@ -172,7 +172,7 @@ Wybierz profil startowy. Wszystko możesz zmienić później w Ustawieniach → Prywatność. - Minimalizacja danych (zalecane) + Minimalizacja danych Przechowywane są tylko twoje własne rozmowy: tells, grupa, FC, linkshells, cross-world linkshells, sojusz i ExtraChat. Czat publiczny, dialogi NPC i spam systemowy są odrzucane na poziomie zapisu. Czas przechowywania według domyślnych wartości specyfikacji (tells 365 dni, własne kanały rozmów 90 dni). @@ -214,7 +214,13 @@ Witaj w Hellion Chat - Fork Chat 2 od Hellion Forge z domyślnymi ustawieniami chroniącymi prywatność, spójną identyfikacją wizualną i drobnymi usprawnieniami komfortu gry. + Twoje okno czatu od Hellion Forge. Prywatność od pierwszego uruchomienia, 25 języków, a układ ustawiasz sam. + + + Hellion Chat powstał jako fork Chat 2. Od tamtej pory oba projekty rozeszły się na tyle, że ich bazy kodu nie są już zgodne. + + + Pluginy to w Final Fantasy XIV szara strefa: warunki korzystania Square Enix ich nie obejmują, a Naoki Yoshida publicznie prosił, by ich nie reklamować. Nie poruszaj więc tego tematu na Say, Yell, Shout ani na żadnym innym kanale publicznym. Trzy krótkie kroki. Wszystko możesz zmienić później w Ustawieniach → Hellion Chat. @@ -228,8 +234,8 @@ Co ma być zapisywane? - - ★ = zalecane dla większości graczy. + + Zalecane Roleplay @@ -298,7 +304,7 @@ (bez zmian) - 💡 Wypróbuj: wpisz /tell <Nazwa gracza> w czacie. Hellion Chat otworzy dedykowaną zakładkę dla rozmowy i wstępnie wczyta ostatnie {0} wiadomości. + Wypróbuj: wpisz /tell <Nazwa gracza> w czacie. Hellion Chat otworzy dedykowaną zakładkę dla rozmowy i wstępnie wczyta ostatnie {0} wiadomości. Ustawienia → Hellion Chat, aby dostosować później @@ -605,6 +611,9 @@ System + + Emoty + Free Company diff --git a/HellionChat/Resources/HellionStrings.pt-BR.resx b/HellionChat/Resources/HellionStrings.pt-BR.resx index fe29447..9db5d6b 100644 --- a/HellionChat/Resources/HellionStrings.pt-BR.resx +++ b/HellionChat/Resources/HellionStrings.pt-BR.resx @@ -172,7 +172,7 @@ Escolha um perfil inicial. Você pode ajustar tudo depois em Configurações → Privacidade. - Minimização de dados (recomendado) + Minimização de dados Apenas suas próprias conversas são armazenadas: tells, grupo, FC, linkshells, cross-world linkshells, aliança e ExtraChat. Bate-papo público, diálogos de NPC e spam de sistema são descartados no nível de armazenamento. A retenção segue os padrões da spec (tells 365 dias, canais de conversa próprios 90 dias). @@ -214,7 +214,13 @@ Bem-vindo ao Hellion Chat - Um fork do Chat 2 pela Hellion Forge com padrões voltados para privacidade, visual consistente com a marca e alguns toques de qualidade de vida. + Sua janela de chat, da Hellion Forge. Privacidade desde o primeiro uso, 25 idiomas, e você organiza do seu jeito. + + + O Hellion Chat começou como um fork do Chat 2. Desde então os dois se afastaram a ponto de as bases de código não serem mais compatíveis. + + + Plugins são uma zona cinzenta no Final Fantasy XIV: os termos de uso da Square Enix não os cobrem, e Naoki Yoshida pediu publicamente que não sejam divulgados. Mantenha o assunto fora de Falar, Grita, Berrar e de qualquer outro canal público. Três passos rápidos. Você pode mudar tudo depois em Configurações → Hellion Chat. @@ -228,8 +234,8 @@ O que será armazenado? - - ★ = recomendado para a maioria dos jogadores. + + Recomendado Roleplay @@ -298,7 +304,7 @@ (sem alteração) - 💡 Experimente: digite /tell <Nome do Jogador> no chat. O Hellion Chat abre uma aba dedicada para a conversa e pré-carrega as últimas {0} mensagens. + Experimente: digite /tell <Nome do Jogador> no chat. O Hellion Chat abre uma aba dedicada para a conversa e pré-carrega as últimas {0} mensagens. Configurações → Hellion Chat para ajustar depois @@ -605,6 +611,9 @@ Sistema + + Emotes + Free Company diff --git a/HellionChat/Resources/HellionStrings.pt-PT.resx b/HellionChat/Resources/HellionStrings.pt-PT.resx index 8a0b209..920b34d 100644 --- a/HellionChat/Resources/HellionStrings.pt-PT.resx +++ b/HellionChat/Resources/HellionStrings.pt-PT.resx @@ -172,7 +172,7 @@ Escolhe um perfil inicial. Podes ajustar tudo depois em Definições → Privacidade. - Minimização de dados (recomendado) + Minimização de dados Só as tuas próprias conversas são armazenadas: tells, grupo, FC, linkshells, cross-world linkshells, aliança e ExtraChat. O chat público, os diálogos de NPC e o spam de sistema são descartados ao nível do armazenamento. A retenção segue os valores predefinidos da spec (tells 365 dias, canais de conversas próprias 90 dias). @@ -214,7 +214,13 @@ Bem-vindo ao Hellion Chat - Um fork do Chat 2 da Hellion Forge com predefinições com privacidade em mente, visuais consistentes com a marca e alguns retoques de qualidade de vida. + A tua janela de conversa, da Hellion Forge. Privacidade desde o primeiro arranque, 25 idiomas, e organizas tudo como quiseres. + + + O Hellion Chat começou como um fork do Chat 2. Desde então os dois afastaram-se ao ponto de as bases de código já não serem compatíveis. + + + Os plugins são uma zona cinzenta no Final Fantasy XIV: os termos de utilização da Square Enix não os abrangem e Naoki Yoshida pediu publicamente que não fossem promovidos. Mantém, portanto, o assunto fora de Say, Yell, Shout e de qualquer outro canal público. Três passos rápidos. Podes mudar tudo depois em Definições → Hellion Chat. @@ -228,8 +234,8 @@ O que fica armazenado? - - ★ = recomendado para a maioria dos jogadores. + + Recomendado Roleplay @@ -298,7 +304,7 @@ (sem alterações) - 💡 Experimenta: escreve /tell <Nome do Jogador> no chat. O Hellion Chat abre um separador dedicado para a conversa e pré-carrega as últimas {0} mensagens. + Experimenta: escreve /tell <Nome do Jogador> no chat. O Hellion Chat abre um separador dedicado para a conversa e pré-carrega as últimas {0} mensagens. Definições → Hellion Chat para ajustar mais tarde @@ -605,6 +611,9 @@ Sistema + + Emotes + Free Company diff --git a/HellionChat/Resources/HellionStrings.resx b/HellionChat/Resources/HellionStrings.resx index 4d12f3d..5e3f01f 100644 --- a/HellionChat/Resources/HellionStrings.resx +++ b/HellionChat/Resources/HellionStrings.resx @@ -172,7 +172,7 @@ Choose a starting profile. You can adjust everything later under Settings → Privacy. - Data minimisation (recommended) + Data minimisation Only your own conversations are stored: tells, party, FC, linkshells, cross-world linkshells, alliance, and ExtraChat. Public chat, NPC dialogues, and system spam are discarded at the storage level. Retention follows spec defaults (tells 365 days, own conversation channels 90 days). @@ -214,7 +214,13 @@ Welcome to Hellion Chat - A Chat 2 fork from Hellion Forge with privacy-aware defaults, brand-consistent visuals, and a few quality-of-life touches. + Your chat window, from Hellion Forge. Privacy-first out of the box, translated into 25 languages, and yours to arrange. + + + Plugins are a grey area in Final Fantasy XIV: Square Enix's terms of service do not cover them, and Naoki Yoshida has publicly asked that people not advertise them. Keep the subject out of Say, Yell, Shout and every other public channel. + + + Hellion Chat started out as a fork of Chat 2. The two have drifted far enough apart since that the codebases are no longer compatible. Three short steps. You can change everything later under Settings → Hellion Chat. @@ -228,8 +234,8 @@ What gets stored? - - ★ = recommended for most players. + + Recommended Roleplay @@ -298,7 +304,7 @@ (unchanged) - 💡 Try it: type /tell <Player Name> into chat. Hellion Chat opens a dedicated tab for the conversation and preloads the last {0} messages. + Try it: type /tell <Player Name> into chat. Hellion Chat opens a dedicated tab for the conversation and preloads the last {0} messages. Settings → Hellion Chat to fine-tune later @@ -598,6 +604,9 @@ System + + Emotes + Free Company diff --git a/HellionChat/Resources/HellionStrings.ro.resx b/HellionChat/Resources/HellionStrings.ro.resx index 2e97185..fe6d5cd 100644 --- a/HellionChat/Resources/HellionStrings.ro.resx +++ b/HellionChat/Resources/HellionStrings.ro.resx @@ -172,7 +172,7 @@ Alege un profil de start. Poți ajusta orice mai târziu din Setări → Confidențialitate. - Minimizarea datelor (recomandat) + Minimizarea datelor Sunt stocate doar propriile tale conversații: tells, party, FC, linkshells, cross-world linkshells, alliance și ExtraChat. Chatul public, dialogurile NPC și spam-ul de sistem sunt respinse la nivelul stocării. Retenția urmează implicite spec (tells 365 zile, canale de conversație proprii 90 zile). @@ -214,7 +214,13 @@ Bun venit în Hellion Chat - Un fork Chat 2 de la Hellion Forge cu setări implicite orientate spre confidențialitate, aspect consecvent cu brandul și câteva îmbunătățiri de calitate a vieții. + Fereastra ta de chat, de la Hellion Forge. Confidențialitate din start, 25 de limbi, iar aranjarea îți aparține. + + + Hellion Chat a pornit ca un fork al Chat 2. De atunci cele două s-au îndepărtat suficient încât bazele de cod nu mai sunt compatibile. + + + Pluginurile sunt o zonă gri în Final Fantasy XIV: termenii de utilizare Square Enix nu le acoperă, iar Naoki Yoshida a cerut public să nu fie promovate. Ține deci subiectul departe de Say, Yell, Shout și de orice alt canal public. Trei pași scurți. Poți schimba orice mai târziu din Setări → Hellion Chat. @@ -228,8 +234,8 @@ Ce se stochează? - - ★ = recomandat pentru cei mai mulți jucători. + + Recomandat Roleplay @@ -298,7 +304,7 @@ (nemodificat) - 💡 Încearcă: tastează /tell <Nume Jucător> în chat. Hellion Chat deschide un tab dedicat pentru conversație și preîncarcă ultimele {0} mesaje. + Încearcă: tastează /tell <Nume Jucător> în chat. Hellion Chat deschide un tab dedicat pentru conversație și preîncarcă ultimele {0} mesaje. Setări → Hellion Chat pentru ajustări ulterioare @@ -605,6 +611,9 @@ System + + Emotes + Free Company diff --git a/HellionChat/Resources/HellionStrings.ru.resx b/HellionChat/Resources/HellionStrings.ru.resx index d3e84fd..42b2d48 100644 --- a/HellionChat/Resources/HellionStrings.ru.resx +++ b/HellionChat/Resources/HellionStrings.ru.resx @@ -172,7 +172,7 @@ Выберите начальный профиль. Всё можно изменить позже в разделе Настройки → Конфиденциальность. - Минимизация данных (рекомендуется) + Минимизация данных Сохраняются только ваши собственные разговоры: ЛС, группа, свободная компания, Linkshells, межмировые Linkshells, альянс и ExtraChat. Публичный чат, диалоги NPC и системный спам отбрасываются на уровне хранения. Сроки хранения соответствуют значениям по умолчанию (ЛС — 365 дней, собственные каналы разговора — 90 дней). @@ -214,7 +214,13 @@ Добро пожаловать в Hellion Chat - Форк Chat 2 от Hellion Forge с настройками конфиденциальности по умолчанию, фирменным оформлением и рядом улучшений удобства использования. + Твоё окно чата от Hellion Forge. Приватность по умолчанию, 25 языков, и раскладка полностью в твоих руках. + + + Hellion Chat начинался как форк Chat 2. С тех пор проекты разошлись настолько, что кодовые базы больше не совместимы. + + + Плагины в Final Fantasy XIV находятся в серой зоне: условия использования Square Enix их не охватывают, а Наоки Ёсида публично просил не рекламировать их. Не поднимай эту тему в каналах Сказать, Вопль, Крик и любых других публичных. Три коротких шага. Всё можно изменить позже в разделе Настройки → Hellion Chat. @@ -228,8 +234,8 @@ Что будет сохраняться? - - ★ = рекомендуется для большинства игроков. + + Рекомендовано Roleplay @@ -298,7 +304,7 @@ (без изменений) - 💡 Попробуйте: введите /tell <Имя игрока> в чате. Hellion Chat откроет отдельную вкладку для разговора и предзагрузит последние {0} сообщений. + Попробуйте: введите /tell <Имя игрока> в чате. Hellion Chat откроет отдельную вкладку для разговора и предзагрузит последние {0} сообщений. Настройки → Hellion Chat для тонкой настройки позже @@ -605,6 +611,9 @@ Система + + Эмоции + Свободная компания diff --git a/HellionChat/Resources/HellionStrings.sv.resx b/HellionChat/Resources/HellionStrings.sv.resx index 62059bc..a3dde99 100644 --- a/HellionChat/Resources/HellionStrings.sv.resx +++ b/HellionChat/Resources/HellionStrings.sv.resx @@ -172,7 +172,7 @@ Välj en startprofil. Du kan justera allt senare under Inställningar → Sekretess. - Dataminimering (rekommenderas) + Dataminimering Bara dina egna konversationer sparas: tells, grupp, FC, linkshells, flervärlds-linkshells, allians och ExtraChat. Offentlig chatt, NPC-dialoger och systemskräp kasseras på lagringsnivå. Lagring följer spec-standarder (tells 365 dagar, egna konversationskanaler 90 dagar). @@ -214,7 +214,13 @@ Välkommen till Hellion Chat - En Chat 2-fork från Hellion Forge med sekretessmedvetna standardinställningar, konsekvent varumärkesutseende och några livskvalitetsförbättringar. + Ditt chattfönster från Hellion Forge. Integritet från första start, 25 språk, och du ställer in det som du vill. + + + Hellion Chat började som en fork av Chat 2. De två har sedan dess glidit isär så mycket att kodbaserna inte längre är kompatibla. + + + Plugins är en gråzon i Final Fantasy XIV: Square Enix användarvillkor täcker dem inte, och Naoki Yoshida har offentligt bett om att man inte gör reklam för dem. Håll därför ämnet borta från Säg, Skrik, Ropa och alla andra offentliga kanaler. Tre korta steg. Du kan ändra allt senare under Inställningar → Hellion Chat. @@ -228,8 +234,8 @@ Vad sparas? - - ★ = rekommenderas för de flesta spelare. + + Rekommenderad Roleplay @@ -298,7 +304,7 @@ (oförändrat) - 💡 Prova: skriv /tell <Spelarnamn> i chatten. Hellion Chat öppnar en dedikerad flik för konversationen och förladdar de senaste {0} meddelandena. + Prova: skriv /tell <Spelarnamn> i chatten. Hellion Chat öppnar en dedikerad flik för konversationen och förladdar de senaste {0} meddelandena. Inställningar → Hellion Chat för att finjustera senare @@ -605,6 +611,9 @@ System + + Emotes + Free Company diff --git a/HellionChat/Resources/HellionStrings.tr.resx b/HellionChat/Resources/HellionStrings.tr.resx index 5d8b520..49b7ff2 100644 --- a/HellionChat/Resources/HellionStrings.tr.resx +++ b/HellionChat/Resources/HellionStrings.tr.resx @@ -172,7 +172,7 @@ Bir başlangıç profili seç. Her şeyi daha sonra Ayarlar → Gizlilik altında değiştirebilirsin. - Veri minimizasyonu (önerilen) + Veri minimizasyonu Yalnızca kendi konuşmaların saklanır: tell'ler, parti, FC, linkshell'ler, cross-world linkshell'ler, alliance ve ExtraChat. Genel sohbet, NPC diyalogları ve sistem spam'i depolama düzeyinde atılır. Saklama süresi spec varsayılanlarına göre ayarlanır (tell'ler 365 gün, kendi konuşma kanalları 90 gün). @@ -214,7 +214,13 @@ Hellion Chat'e hoş geldin - Hellion Forge'dan gizlilik odaklı varsayılanlar, marka tutarlı görsel tasarım ve birkaç kullanım kolaylığı dokunuşuyla Chat 2'nin bir fork'u. + Hellion Forge'un sohbet penceresi. İlk açılıştan itibaren gizlilik öncelikli, 25 dilde, ve düzenini kendin kurarsın. + + + Hellion Chat, Chat 2'nin bir fork'u olarak başladı. İkisi o zamandan beri kod tabanları artık uyumlu olmayacak kadar birbirinden uzaklaştı. + + + Pluginler Final Fantasy XIV'te gri bir alandadır: Square Enix'in kullanım koşulları onları kapsamaz ve Naoki Yoshida bunların tanıtılmamasını açıkça rica etmiştir. Bu yüzden konuyu Say, Yell, Shout ve diğer tüm herkese açık kanalların dışında tut. Üç kısa adım. Her şeyi daha sonra Ayarlar → Hellion Chat altında değiştirebilirsin. @@ -228,8 +234,8 @@ Ne saklanacak? - - ★ = çoğu oyuncu için önerilen. + + Önerilen Roleplay @@ -298,7 +304,7 @@ (değiştirilmedi) - 💡 Dene: sohbete /tell <Oyuncu Adı> yaz. Hellion Chat konuşma için özel bir sekme açar ve son {0} mesajı önceden yükler. + Dene: sohbete /tell <Oyuncu Adı> yaz. Hellion Chat konuşma için özel bir sekme açar ve son {0} mesajı önceden yükler. Daha sonra ince ayar için Ayarlar → Hellion Chat @@ -605,6 +611,9 @@ Sistem + + Emote'lar + Free Company diff --git a/HellionChat/Resources/HellionStrings.uk.resx b/HellionChat/Resources/HellionStrings.uk.resx index 4274ec3..b9e2488 100644 --- a/HellionChat/Resources/HellionStrings.uk.resx +++ b/HellionChat/Resources/HellionStrings.uk.resx @@ -172,7 +172,7 @@ Виберіть початковий профіль. Все можна налаштувати пізніше в розділі Налаштування → Конфіденційність. - Мінімізація даних (рекомендовано) + Мінімізація даних Зберігаються лише Ваші власні розмови: tells, група, FC, linkshells, cross-world linkshells, альянс і ExtraChat. Публічний чат, діалоги NPC та системний спам відкидаються на рівні зберігання. Термін зберігання за стандартними значеннями специфікації (tells — 365 днів, канали власних розмов — 90 днів). @@ -214,7 +214,13 @@ Ласкаво просимо до Hellion Chat - Форк Chat 2 від Hellion Forge з урахуванням конфіденційності, фірмовим оформленням та кількома зручними покращеннями. + Твоє вікно чату від Hellion Forge. Приватність за замовчуванням, 25 мов, і розкладка цілком у твоїх руках. + + + Hellion Chat починався як форк Chat 2. Відтоді проєкти розійшлися настільки, що кодові бази більше не сумісні. + + + Плагіни у Final Fantasy XIV перебувають у сірій зоні: умови використання Square Enix їх не охоплюють, а Наокі Йосіда публічно просив не рекламувати їх. Не піднімай цю тему в каналах Say, Yell, Shout та будь-яких інших публічних. Три коротких кроки. Все можна змінити пізніше в розділі Налаштування → Hellion Chat. @@ -228,8 +234,8 @@ Що зберігається? - - ★ = рекомендовано для більшості гравців. + + Рекомендовано Roleplay @@ -298,7 +304,7 @@ (без змін) - 💡 Спробуйте: введіть /tell <Ім'я гравця> у чат. Hellion Chat відкриє окрему вкладку для розмови й попередньо завантажить останні {0} повідомлень. + Спробуйте: введіть /tell <Ім'я гравця> у чат. Hellion Chat відкриє окрему вкладку для розмови й попередньо завантажить останні {0} повідомлень. Налаштування → Hellion Chat для подальшого тонкого налаштування @@ -605,6 +611,9 @@ Система + + Емоти + Free Company diff --git a/HellionChat/Resources/HellionStrings.zh-Hans.resx b/HellionChat/Resources/HellionStrings.zh-Hans.resx index 5605975..074af34 100644 --- a/HellionChat/Resources/HellionStrings.zh-Hans.resx +++ b/HellionChat/Resources/HellionStrings.zh-Hans.resx @@ -172,7 +172,7 @@ 选择一个初始配置方案。之后可在设置 → 隐私中随时调整。 - 数据最小化(推荐) + 数据最小化 仅保存你自己的对话:密语、小队、部队、通讯贝、跨服通讯贝、团队以及 ExtraChat。公共聊天、NPC 对话和系统垃圾信息将在存储层直接丢弃。保留期限遵循规格默认值(密语 365 天,自有对话频道 90 天)。 @@ -214,7 +214,13 @@ 欢迎使用 Hellion Chat - 来自 Hellion Forge 的 Chat 2 分支,具备隐私友好的默认设置、品牌一致的视觉风格,以及若干实用改进。 + 来自 Hellion Forge 的聊天窗口。默认即注重隐私,支持 25 种语言,界面由你自己安排。 + + + Hellion Chat 最初是 Chat 2 的分支。此后两者已相去甚远,代码库不再兼容。 + + + 插件在《最终幻想14》中处于灰色地带:史克威尔艾尼克斯的使用条款并未涵盖插件,吉田直树也曾公开呼吁不要宣传。请不要在说话、呼喊、喊话以及其他任何公开频道谈论此事。 共三个简短步骤。之后可在设置 → Hellion Chat 中随时修改。 @@ -228,8 +234,8 @@ 保存哪些内容? - - ★ = 推荐大多数玩家使用。 + + 推荐 Roleplay @@ -298,7 +304,7 @@ (未更改) - 💡 试一试:在聊天框输入 /tell <玩家名称>。Hellion Chat 会自动为该对话开启专属标签页,并预加载最近 {0} 条消息。 + 试一试:在聊天框输入 /tell <玩家名称>。Hellion Chat 会自动为该对话开启专属标签页,并预加载最近 {0} 条消息。 进入设置 → Hellion Chat 可进一步微调 @@ -605,6 +611,9 @@ 系统 + + 情感动作 + 部队 diff --git a/HellionChat/Resources/HellionStrings.zh-Hant.resx b/HellionChat/Resources/HellionStrings.zh-Hant.resx index 337e273..c399412 100644 --- a/HellionChat/Resources/HellionStrings.zh-Hant.resx +++ b/HellionChat/Resources/HellionStrings.zh-Hant.resx @@ -172,7 +172,7 @@ 選擇一個起始設定檔。之後可在設定 → 隱私中調整所有選項。 - 資料最小化(推薦) + 資料最小化 只儲存你自己的對話:悄悄話、小隊、部隊、通訊貝、跨服通訊貝、團隊和 ExtraChat。公開聊天、NPC 對話和系統垃圾訊息在儲存層即被丟棄。保留期限遵循規格預設值(悄悄話 365 天,自己的對話頻道 90 天)。 @@ -214,7 +214,13 @@ 歡迎使用 Hellion Chat - Hellion Forge 推出的 Chat 2 分支版本,具備重視隱私的預設值、品牌一致的視覺設計以及一些生活品質改善。 + 來自 Hellion Forge 的聊天視窗。預設即重視隱私,支援 25 種語言,版面由你自己安排。 + + + Hellion Chat 最初是 Chat 2 的分支。此後兩者已相去甚遠,程式碼庫不再相容。 + + + 外掛在《Final Fantasy XIV》中屬於灰色地帶:史克威爾艾尼克斯的使用條款並未涵蓋,吉田直樹也曾公開呼籲不要宣傳。請勿在説話、呼喊、喊話以及其他任何公開頻道談論此事。 共三個簡短步驟。之後可在設定 → Hellion Chat 中變更所有設定。 @@ -228,8 +234,8 @@ 哪些內容會被儲存? - - ★ = 推薦給大多數玩家。 + + 推薦 角色扮演 @@ -298,7 +304,7 @@ (未變更) - 💡 試試看:在聊天中輸入 /tell <玩家名稱>。Hellion Chat 會為此對話開啟專屬標籤頁,並預載最後 {0} 則訊息。 + 試試看:在聊天中輸入 /tell <玩家名稱>。Hellion Chat 會為此對話開啟專屬標籤頁,並預載最後 {0} 則訊息。 設定 → Hellion Chat 可在之後進行細部調整 @@ -605,6 +611,9 @@ 系統 + + 情感動作 + 部隊 diff --git a/HellionChat/Resources/Language.es.resx b/HellionChat/Resources/Language.es.resx index 2676c71..976ef85 100644 --- a/HellionChat/Resources/Language.es.resx +++ b/HellionChat/Resources/Language.es.resx @@ -731,7 +731,7 @@ Decir - Shout + Vociferar Tell (saliente) diff --git a/HellionChat/SelfTests/CardClipPlanStep.cs b/HellionChat/SelfTests/CardClipPlanStep.cs index e90d5fa..7a51849 100644 --- a/HellionChat/SelfTests/CardClipPlanStep.cs +++ b/HellionChat/SelfTests/CardClipPlanStep.cs @@ -5,7 +5,7 @@ using HellionChat.Util; namespace HellionChat.SelfTests; -// B2: behavioural check that the card path feeds CardClipPlanner AND that a +// Behavioural check that the card path feeds CardClipPlanner AND that a // layout change clears the height cache — not a non-null check. Pure plan math // is pinned headless by CardClipPlanTests; this drives the live accessors. internal sealed class CardClipPlanStep : ISelfTestStep @@ -56,7 +56,7 @@ internal sealed class CardClipPlanStep : ISelfTestStep int remaining; try { - // v1.10.0/A1: the fingerprint gate waits for the value to settle, so + // v1.10.0: the fingerprint gate waits for the value to settle, so // the step walks a synthetic clock past the window instead of sleeping. var clock = Environment.TickCount64; messages.RunHeightCacheInvalidationForSelfTest(tab, 400f, clock); diff --git a/HellionChat/SelfTests/ConfigMigrationV26Step.cs b/HellionChat/SelfTests/ConfigMigrationV27Step.cs similarity index 74% rename from HellionChat/SelfTests/ConfigMigrationV26Step.cs rename to HellionChat/SelfTests/ConfigMigrationV27Step.cs index a64d0ea..70de61d 100644 --- a/HellionChat/SelfTests/ConfigMigrationV26Step.cs +++ b/HellionChat/SelfTests/ConfigMigrationV27Step.cs @@ -4,24 +4,37 @@ using Dalamud.Plugin.SelfTest; namespace HellionChat.SelfTests; // Pins the post-migration shape of the config. By /xlperf time the schema gate -// has already stamped Config.Version and run both migrations, so the fields -// below must carry valid values here. This probe never rewrites config; the -// migrations themselves are load-time and verified by the prepared-config smoke -// in the plan. -internal sealed class ConfigMigrationV26Step : ISelfTestStep +// has stamped Config.Version and the 2.0.0 reset has run, so the fields below +// must carry valid values here. This probe never rewrites config; the reset +// itself is load-time. +internal sealed class ConfigMigrationV27Step : ISelfTestStep { - public ConfigMigrationV26Step(Plugin plugin) + public ConfigMigrationV27Step(Plugin plugin) { _ = plugin; } - public string Name => "Hellion Chat - Config v26 migration"; + public string Name => "Hellion Chat - Config v27 migration"; public SelfTestStepResult RunStep() { - if (Plugin.Config.Version != 26) + if (Plugin.Config.Version != 27) { - ImGui.Text($"Config.Version is {Plugin.Config.Version}, expected 26"); + ImGui.Text($"Config.Version is {Plugin.Config.Version}, expected 27"); + return SelfTestStepResult.Fail; + } + + // v27 replaces the config rather than migrating it, and CreateFresh + // hands back an empty tab list. The seeding in LoadAsync is what fills + // it, and it runs in a different method than the reset does -- if that + // ordering ever breaks, every user comes out of the update with no tabs + // at all and nothing else in the plugin would notice. + if (Plugin.Config.Tabs.Count == 0) + { + ImGui.Text( + "No tabs at all. The v27 reset empties the list and LoadAsync is what " + + "seeds the presets back -- reaching this point empty means it did not." + ); return SelfTestStepResult.Fail; } diff --git a/HellionChat/SelfTests/CurrentTabCouplingStep.cs b/HellionChat/SelfTests/CurrentTabCouplingStep.cs index aa36c7e..fdf95a8 100644 --- a/HellionChat/SelfTests/CurrentTabCouplingStep.cs +++ b/HellionChat/SelfTests/CurrentTabCouplingStep.cs @@ -3,7 +3,7 @@ using Dalamud.Plugin.SelfTest; namespace HellionChat.SelfTests; -// F2: CurrentTab is coupled to MainWindow.ActiveTab (no longer the fixed index-0 +// CurrentTab is coupled to MainWindow.ActiveTab (no longer the fixed index-0 // Tabs lookup). Asserts ReferenceEquals between the two, with false-green // defenses: (1) empty-config exercises the getter's fallback; (2) null ActiveTab // opens the window so the Draw-seed sets it and retries via Waiting (bounded so a diff --git a/HellionChat/SelfTests/CurrentTabGuidedStep.cs b/HellionChat/SelfTests/CurrentTabGuidedStep.cs index dfb8b45..f3fbe5e 100644 --- a/HellionChat/SelfTests/CurrentTabGuidedStep.cs +++ b/HellionChat/SelfTests/CurrentTabGuidedStep.cs @@ -5,17 +5,17 @@ using HellionChat.GameFunctions.Types; namespace HellionChat.SelfTests; -// F2 (guided): interactive, fires NO synthetic probes. Shows the full measured +// Guided: interactive, fires NO synthetic probes. Shows the full measured // state every frame so a result is observable, not a guess, and walks the user // through the real switch-away-and-back flow. It verifies the PRIVACY-relevant // effect, keyed on the tab type: // - a NORMAL tab carrying a game-side tell must lose its RUNTIME target -// (CurrentChannel.TellTarget) on switch-away-and-back (the F1 strip), so a +// (CurrentChannel.TellTarget) on switch-away-and-back, so a // typed line can't /tell the old partner; // - a BOUND auto-tell tab keeps its partner by design (leg1) — its binding is // Tab.TellTarget and is deliberately untouched by the strip. // The channel label is intentionally NOT asserted: a tell tab re-derives back to -// Tell after the strip (spec TR-7); only the target matters for privacy. +// Tell after the strip ; only the target matters for privacy. internal sealed class CurrentTabGuidedStep : ISelfTestStep { private readonly Plugin _plugin; diff --git a/HellionChat/SelfTests/DisclosureArmStep.cs b/HellionChat/SelfTests/DisclosureArmStep.cs index 6e2fb9c..2d4a184 100644 --- a/HellionChat/SelfTests/DisclosureArmStep.cs +++ b/HellionChat/SelfTests/DisclosureArmStep.cs @@ -5,7 +5,7 @@ using HellionChat._Helpers; namespace HellionChat.SelfTests; -// B2-3: proves the plugin-disclosure arm-and-hold wires the (otherwise verwaist) +// Proves the plugin-disclosure arm-and-hold wires the (otherwise verwaist) // scanner into the REAL send entry InputBar.TrySend. Drives TrySend via the // arm-test-hook with a PUA glyph in the buffer and NotifyPluginDisclosure on: // the first send must ARM and HOLD (no send), so PendingMessage stays the probe diff --git a/HellionChat/SelfTests/ExportRoundTripStep.cs b/HellionChat/SelfTests/ExportRoundTripStep.cs index 7c9c0be..0d8d194 100644 --- a/HellionChat/SelfTests/ExportRoundTripStep.cs +++ b/HellionChat/SelfTests/ExportRoundTripStep.cs @@ -7,7 +7,7 @@ using HellionChat.Util; namespace HellionChat.SelfTests; -// v1.12.0/A2: the exporter now reads text from the chunk lists instead of the +// v1.12.0: the exporter now reads text from the chunk lists instead of the // raw SeStrings. That change is invisible to the build suite -- ExportToFile // takes IEnumerable, Message needs SeString, and xUnit cannot load // Dalamud.dll, so even an empty list fails before the body runs. diff --git a/HellionChat/SelfTests/FontManagerCtorSmokeStep.cs b/HellionChat/SelfTests/FontManagerCtorSmokeStep.cs index f9a30fe..e05b9bf 100644 --- a/HellionChat/SelfTests/FontManagerCtorSmokeStep.cs +++ b/HellionChat/SelfTests/FontManagerCtorSmokeStep.cs @@ -112,7 +112,7 @@ internal sealed class FontManagerCtorSmokeStep : ISelfTestStep return SelfTestStepResult.Fail; } - // B1: assert the atlas actually finished building all required handles, + // Assert the atlas actually finished building all required handles, // not just that the references are non-null. FontsReady is the observable // state the trimmed-fallback rebuild must still reach; a half-built atlas // would pass the null/exception checks above but fail here. @@ -127,8 +127,8 @@ internal sealed class FontManagerCtorSmokeStep : ISelfTestStep return SelfTestStepResult.Fail; } - // Report what was actually verified (Flo's request: don't just show Pass). - // The glyph-range entry counts make the B1 dedup visible — the cjk-fallback + // Report what was actually verified rather than a bare Pass. + // The glyph-range entry counts make the dedup visible — the cjk-fallback // range is now a small trimmed remainder next to the large primary range. var counts = fm.GlyphRangeLengths; var italicState = diff --git a/HellionChat/SelfTests/GlobalStyleScopeAllocStep.cs b/HellionChat/SelfTests/GlobalStyleScopeAllocStep.cs index 5172223..4bb541d 100644 --- a/HellionChat/SelfTests/GlobalStyleScopeAllocStep.cs +++ b/HellionChat/SelfTests/GlobalStyleScopeAllocStep.cs @@ -43,7 +43,7 @@ internal sealed class GlobalStyleScopeAllocStep : ISelfTestStep GlobalStyleScope.Push(theme, registry, opacity).Dispose(); var delta = GC.GetAllocatedBytesForCurrentThread() - before; - // Report the measured figure on BOTH outcomes (Flo's request: don't just + // Report the measured figure on BOTH outcomes (a bare Pass hides // show Pass) — the byte delta is the whole point of the GC-reserve probe. var ok = delta <= AllocBudgetBytes; var status = ok ? "PASS" : "FAIL"; diff --git a/HellionChat/SelfTests/HideRestoreSelfTestStep.cs b/HellionChat/SelfTests/HideRestoreSelfTestStep.cs index cb5ebf9..0496819 100644 --- a/HellionChat/SelfTests/HideRestoreSelfTestStep.cs +++ b/HellionChat/SelfTests/HideRestoreSelfTestStep.cs @@ -4,7 +4,7 @@ using HellionChat.Ui.Windows; namespace HellionChat.SelfTests; -// P8 wiring: UserHide() suppresses DrawConditions; both ActivateChat() (Enter) and +// wiring: UserHide() suppresses DrawConditions; both ActivateChat() (Enter) and // Toggle() (/hellion) restore it. Pure window-state — the focus side is left to smoke. internal sealed class HideRestoreSelfTestStep : ISelfTestStep { diff --git a/HellionChat/SelfTests/HonorificHeaderRenderStep.cs b/HellionChat/SelfTests/HonorificHeaderRenderStep.cs index 1f10a32..57460f5 100644 --- a/HellionChat/SelfTests/HonorificHeaderRenderStep.cs +++ b/HellionChat/SelfTests/HonorificHeaderRenderStep.cs @@ -68,7 +68,7 @@ internal sealed class HonorificHeaderRenderStep : ISelfTestStep ); // Draw at a deliberately wide 420px so the title never hits the truncation - // clamp — LastTitleRendered then reflects the GATE outcome, not the width. + // clamp -- LastTitleRendered then reflects the gate outcome, not the width. try { // (a) available + valid title + toggle on -> title renders diff --git a/HellionChat/SelfTests/HoverStateFootprintStep.cs b/HellionChat/SelfTests/HoverStateFootprintStep.cs index a8c9d43..3eba1b0 100644 --- a/HellionChat/SelfTests/HoverStateFootprintStep.cs +++ b/HellionChat/SelfTests/HoverStateFootprintStep.cs @@ -4,7 +4,7 @@ using HellionChat.Ui.StyleEngine; namespace HellionChat.SelfTests; -// Master-spec §7.5 scope note: the hover registry must not grow frame by frame. +// the hover registry must not grow frame by frame. // Successor to HoverSheenAllocStep, which pinned the same contract against the // old sheen start-timestamp dictionary. // diff --git a/HellionChat/SelfTests/MainWindowFlagsStep.cs b/HellionChat/SelfTests/MainWindowFlagsStep.cs index 9462c39..0335fd1 100644 --- a/HellionChat/SelfTests/MainWindowFlagsStep.cs +++ b/HellionChat/SelfTests/MainWindowFlagsStep.cs @@ -4,11 +4,11 @@ using HellionChat.Ui.Windows; namespace HellionChat.SelfTests; -// B1-2 window flags. Drives the REAL MainWindow.PreDraw and asserts it wired +// window flags. Drives the REAL MainWindow.PreDraw and asserts it wired // Window.Flags to ResolveFlags(CanMove, CanResize), then pins the pure // fresh-base contract: false/false adds NoMove|NoResize, true/true clears them -// (the masterplan's "flags must rebuild from a fresh base, else NoMove sticks -// after toggling back" risk). NoScrollbar|NoScrollWithMouse always present. +// -- flags must rebuild from a fresh base, or NoMove sticks after toggling +// back. NoScrollbar|NoScrollWithMouse always present. // Non-test caller of ResolveFlags: MainWindow.PreDraw. internal sealed class MainWindowFlagsStep : ISelfTestStep { @@ -72,7 +72,7 @@ internal sealed class MainWindowFlagsStep : ISelfTestStep return SelfTestStepResult.Fail; } - // P7 title-bar contract: ShowTitleBar=false adds NoTitleBar from the + // title-bar contract: ShowTitleBar=false adds NoTitleBar from the // fresh base, true clears it (same no-accumulation guarantee). var barHidden = MainWindow.ResolveFlags(true, true, false); var barShown = MainWindow.ResolveFlags(true, true, true); diff --git a/HellionChat/SelfTests/MainWindowFocusOpacityStep.cs b/HellionChat/SelfTests/MainWindowFocusOpacityStep.cs index 0076e0a..14fe67d 100644 --- a/HellionChat/SelfTests/MainWindowFocusOpacityStep.cs +++ b/HellionChat/SelfTests/MainWindowFocusOpacityStep.cs @@ -3,7 +3,7 @@ using Dalamud.Plugin.SelfTest; namespace HellionChat.SelfTests; -// UI-12 focus opacity. Pins the pure ResolveBgAlpha contract (focused → +// focus opacity. Pins the pure ResolveBgAlpha contract (focused → // WindowOpacity, unfocused → WindowOpacityInactive). The PreDraw wiring // (BgAlpha = ResolveBgAlpha(IsFocused) behind the main-viewport/!docked guard) // is NOT headless-deterministic — the guard may leave BgAlpha null when diff --git a/HellionChat/SelfTests/NotificationSoundSelectStep.cs b/HellionChat/SelfTests/NotificationSoundSelectStep.cs index 7009fdf..786dfa3 100644 --- a/HellionChat/SelfTests/NotificationSoundSelectStep.cs +++ b/HellionChat/SelfTests/NotificationSoundSelectStep.cs @@ -7,7 +7,7 @@ using HellionChat.Util; namespace HellionChat.SelfTests; -// B3-3: notification-sound selection. Drives the pure SelectNotificationSound +// Notification-sound selection. Drives the pure SelectNotificationSound // (the exact pick logic ProcessMessage runs per message) through its SelfTest // wrapper with local synthetic tabs — Plugin.Config.Tabs is never touched, so // no real tab gains messages or unread state. The audible preview button is diff --git a/HellionChat/SelfTests/PerformanceBaselineLog.cs b/HellionChat/SelfTests/PerformanceBaselineLog.cs index 20f396b..38a8925 100644 --- a/HellionChat/SelfTests/PerformanceBaselineLog.cs +++ b/HellionChat/SelfTests/PerformanceBaselineLog.cs @@ -3,11 +3,11 @@ using System.IO; namespace HellionChat.SelfTests; -// Disk sink for the B5 performance baseline. Kept separate from the SelfTest +// Disk sink for the performance baseline. Kept separate from the SelfTest // step so the per-frame hot path never references file IO. Writes one // perf-baseline.json into the plugin ConfigDirectory, atomically (tmp + move) // like ThemeRegistry's theme writer, so a mid-write crash leaves either the -// old file or the new file, never a half JSON. Field names track §7.5: +// old file or the new file, never a half JSON. Field names track the performance-baseline layout: // steady-state Draw cost (avg/max ms), the quad-proxy draw-call count // (avg/max), and frame delta (avg/max). First-frame-HITCH is read off // drawMs max/avg by the human author, platform-annotated in the notes. diff --git a/HellionChat/SelfTests/PerformanceBaselineStep.cs b/HellionChat/SelfTests/PerformanceBaselineStep.cs index c5512ce..f075905 100644 --- a/HellionChat/SelfTests/PerformanceBaselineStep.cs +++ b/HellionChat/SelfTests/PerformanceBaselineStep.cs @@ -6,19 +6,19 @@ namespace HellionChat.SelfTests; // Optional metric capture. Accumulates 1000 steady-state frames of ImGui IO // counters plus the plugin's full-Draw wall-time (Plugin.LastDrawMs, B5-1), // then writes a single perf-baseline.json into the plugin ConfigDirectory so -// the cycle-notes author can copy the §7.5 figures without a separate +// the cycle-notes author can copy the baseline figures without a separate // profiling harness. The step only records — it never fails on a threshold -// (the budgets are evaluated by a human against the JSON, §7.5 "optional, +// (the budgets are evaluated by a human against the JSON ("optional, // manual"). It returns Waiting until the sample window fills, mirroring the // per-frame poll idiom of ThemeSwitchSelfTestStep. internal sealed class PerformanceBaselineStep : ISelfTestStep { - // §7.5 steady-state window. 1000 frames ≈ 16s at 60fps, long enough to + // Steady-state window. 1000 frames ≈ 16s at 60fps, long enough to // average out GC blips without making the manual step tedious. private const int TargetFrames = 1000; // Rough draw-call proxy: ImGui emits 6 indices per quad, so vertices/6 is an - // intentional under-count of draw work, not the exact quad count (API-3). + // intentional under-count of draw work, not the exact quad count. private const int VerticesPerQuadProxy = 6; private readonly Plugin _plugin; diff --git a/HellionChat/SelfTests/ScrollSnapDecisionStep.cs b/HellionChat/SelfTests/ScrollSnapDecisionStep.cs index 54c3ce5..d0efc1f 100644 --- a/HellionChat/SelfTests/ScrollSnapDecisionStep.cs +++ b/HellionChat/SelfTests/ScrollSnapDecisionStep.cs @@ -3,7 +3,7 @@ using Dalamud.Plugin.SelfTest; namespace HellionChat.SelfTests; -// B3-5: only the snap decision is headless-testable. Scroll detection + bar + +// Only the snap decision is headless-testable. Scroll detection + bar + // hit-test are smoke-only (the scroll child exists only in-game; GetScrollY is // garbage headless). Drives ResolveSnapToBottom via the SelfTest accessor and // asserts the OR + the request reset invariant. diff --git a/HellionChat/SelfTests/SenderNameReformatStep.cs b/HellionChat/SelfTests/SenderNameReformatStep.cs index 2e7c063..999b42c 100644 --- a/HellionChat/SelfTests/SenderNameReformatStep.cs +++ b/HellionChat/SelfTests/SenderNameReformatStep.cs @@ -5,7 +5,7 @@ using Dalamud.Plugin.SelfTest; namespace HellionChat.SelfTests; -// B2-1/B2-2: proves the WorldSuffixMode/NameFormMode reformat reaches the REAL +// Proves the WorldSuffixMode/NameFormMode reformat reaches the REAL // render entry. Drives ChunkRenderer.DrawChunks (a SelfTests/README-sanctioned // real entry that wires SenderNameDisplay.ForDisplay at ChunkRenderer.cs:54) // with a synthetic ChunkSource.Sender chunk carrying a PlayerPayload, at a diff --git a/HellionChat/SelfTests/SidebarActiveSurfaceStep.cs b/HellionChat/SelfTests/SidebarActiveSurfaceStep.cs index 52fd174..73c809c 100644 --- a/HellionChat/SelfTests/SidebarActiveSurfaceStep.cs +++ b/HellionChat/SelfTests/SidebarActiveSurfaceStep.cs @@ -4,7 +4,7 @@ using HellionChat.Code; namespace HellionChat.SelfTests; -// v1.10.0/C3: the active row gets a surface and an accent bar, so exactly the +// v1.10.0: the active row gets a surface and an accent bar, so exactly the // row the user is on must be marked -- and only that one. Drives the real // Sidebar.Draw and reads the render-observability counter, so a regression in // the draw path fails rather than a parallel calculation passing. diff --git a/HellionChat/SelfTests/SidebarGreetedGlyphStep.cs b/HellionChat/SelfTests/SidebarGreetedGlyphStep.cs index c87b875..c7506e8 100644 --- a/HellionChat/SelfTests/SidebarGreetedGlyphStep.cs +++ b/HellionChat/SelfTests/SidebarGreetedGlyphStep.cs @@ -5,7 +5,7 @@ using HellionChat.GameFunctions.Types; namespace HellionChat.SelfTests; -// B3-2: greeted glyph renders only for temp tabs when the toggle is on. Drives +// Greeted glyph renders only for temp tabs when the toggle is on. Drives // the REAL Sidebar.Draw (render precedent: HonorificHeaderRenderStep, the only // real .Draw in this pool — NOT SidebarModeAutoSwitchStep which only calls // IsExpanded/GetWidth) inside the /xlperf window frame and reads the render diff --git a/HellionChat/SelfTests/SidebarModeAutoSwitchStep.cs b/HellionChat/SelfTests/SidebarModeAutoSwitchStep.cs index cb9e41a..88436e9 100644 --- a/HellionChat/SelfTests/SidebarModeAutoSwitchStep.cs +++ b/HellionChat/SelfTests/SidebarModeAutoSwitchStep.cs @@ -58,7 +58,7 @@ internal sealed class SidebarModeAutoSwitchStep : ISelfTestStep return SelfTestStepResult.Fail; } - // B1-3a: the expanded width must come from Config.SidebarWidth, not the + // The expanded width must come from Config.SidebarWidth, not the // old fixed 150 constant. Drive the REAL GetWidth (the single source // Sidebar.Draw consumes) with concrete values and assert the OBSERVED // effect — in-range passthrough plus clamping — instead of mirroring the diff --git a/HellionChat/SelfTests/SidebarSectionHeaderStep.cs b/HellionChat/SelfTests/SidebarSectionHeaderStep.cs index 1915ebd..1ff2c46 100644 --- a/HellionChat/SelfTests/SidebarSectionHeaderStep.cs +++ b/HellionChat/SelfTests/SidebarSectionHeaderStep.cs @@ -5,7 +5,7 @@ using HellionChat.GameFunctions.Types; namespace HellionChat.SelfTests; -// B3-4: section headers render once per non-empty temp-tab pool, and compact +// Section headers render once per non-empty temp-tab pool, and compact // mode suppresses the header text (separators stay). Drives the REAL // Sidebar.Draw inside the /xlperf window frame (same render precedent as // SidebarGreetedGlyphStep) and reads the render observability counter. diff --git a/HellionChat/SelfTests/SidebarUnreadDotStep.cs b/HellionChat/SelfTests/SidebarUnreadDotStep.cs index fc8489d..1900dab 100644 --- a/HellionChat/SelfTests/SidebarUnreadDotStep.cs +++ b/HellionChat/SelfTests/SidebarUnreadDotStep.cs @@ -4,7 +4,7 @@ using HellionChat.Code; namespace HellionChat.SelfTests; -// F3: the unread dot the v1.8.x sidebar rebuild dropped. Drives the REAL +// The unread dot the v1.8.x sidebar rebuild dropped. Drives the REAL // Sidebar.Draw (render precedent: SidebarGreetedGlyphStep) with a probe tab that // is inactive and carries Unread>0, then reads the render-observability counter // so a regressed/absent dot fails. Asserts: dot drawn for an inactive Unseen tab; diff --git a/HellionChat/SelfTests/TabRenamePersistStep.cs b/HellionChat/SelfTests/TabRenamePersistStep.cs index 376bf82..c397d46 100644 --- a/HellionChat/SelfTests/TabRenamePersistStep.cs +++ b/HellionChat/SelfTests/TabRenamePersistStep.cs @@ -4,7 +4,7 @@ using HellionChat.Ui.Components; namespace HellionChat.SelfTests; -// B3-1: rename must persist. Drives the real ApplyTabRename (the InputText +// Rename must persist. Drives the real ApplyTabRename (the InputText // callback path), then SaveConfig + reload from disk and asserts the new name // survived — a fresh-from-config tab, not the same reference (a reference check // would pass on a dead roundtrip). Uses a persistent (non-temp) tab: unpinned diff --git a/HellionChat/SelfTests/TellResetOnActivateStep.cs b/HellionChat/SelfTests/TellResetOnActivateStep.cs index 0784f47..93cb69e 100644 --- a/HellionChat/SelfTests/TellResetOnActivateStep.cs +++ b/HellionChat/SelfTests/TellResetOnActivateStep.cs @@ -7,10 +7,10 @@ using HellionChat.Util; namespace HellionChat.SelfTests; -// F1: the activation strip. Drives the REAL OnTabActivated — the entry the +// The activation strip. Drives the REAL OnTabActivated -- the entry the // Sidebar/TopTabBar click handlers, the pop-out path and the Draw-seed all call // — with local probe tabs (Plugin.Config.Tabs is never touched). Asserts the -// five contracts: strip-on-switch, no-strip-on-reclick (TR-4), leg1 preserve, +// five contracts: strip-on-switch, no-strip-on-reclick, leg1 preserve, // derive, and non-tell untouched. internal sealed class TellResetOnActivateStep : ISelfTestStep { @@ -41,7 +41,7 @@ internal sealed class TellResetOnActivateStep : ISelfTestStep } // (b) re-clicking the already-active tab (previous == tab) must NOT strip - // a live game-tell conversation (TR-4 regression guard). + // a live game-tell conversation (regression guard). var reclick = MakeStaleTellTab(boundTellTarget: false, withLabel: false); TabLifecycleHelpers.OnTabActivated(reclick, reclick); if (reclick.CurrentChannel.TellTarget is null) diff --git a/HellionChat/SelfTests/ThemeCrossfadeSelfTestStep.cs b/HellionChat/SelfTests/ThemeCrossfadeSelfTestStep.cs index d1034a3..598f298 100644 --- a/HellionChat/SelfTests/ThemeCrossfadeSelfTestStep.cs +++ b/HellionChat/SelfTests/ThemeCrossfadeSelfTestStep.cs @@ -4,7 +4,7 @@ using HellionChat.Themes; namespace HellionChat.SelfTests; -// Verifies the v1.5.4 PM-1 crossfade contract: switching the active +// Verifies the v1.5.4 crossfade contract: switching the active // theme arms TryGetActiveCrossfade for ~300ms, then the registry // returns to direct AbgrCache reads. A second switch within 100ms // keeps the lerped path active (no identity-snap). CleanUp restores @@ -67,7 +67,7 @@ internal sealed class ThemeCrossfadeSelfTestStep : ISelfTestStep // it as "saw the start" if more than 300ms have elapsed. // Skip the mid-crossfade-switch phase in that case -- the // lerped path is no longer active, so a second switch would - // re-arm a fresh crossfade and not exercise PM-1b's + // re-arm a fresh crossfade and not exercise its // mid-flight-origin override. if (Environment.TickCount64 - this.armedAtTickMs > 300) { @@ -83,7 +83,7 @@ internal sealed class ThemeCrossfadeSelfTestStep : ISelfTestStep if (!this.sawMidCrossfadeSwitch) { - // PM-Test-3 mid-crossfade-switch phase: within ~100ms of the + // mid-crossfade-switch phase: within ~100ms of the // first observed crossfade, fire a second Switch to a THIRD // theme. ArmCrossfade must compose the current lerped state // as the new origin -- TryGetActiveCrossfade still returns diff --git a/HellionChat/SelfTests/TopTabUnderlineStep.cs b/HellionChat/SelfTests/TopTabUnderlineStep.cs index 1245d8e..345b769 100644 --- a/HellionChat/SelfTests/TopTabUnderlineStep.cs +++ b/HellionChat/SelfTests/TopTabUnderlineStep.cs @@ -4,7 +4,7 @@ using HellionChat.Code; namespace HellionChat.SelfTests; -// v1.10.0/D1: the top-tab strip marks the active tab with a fill plus an accent +// v1.10.0: the top-tab strip marks the active tab with a fill plus an accent // underline. Drives the real TopTabBar.Draw and reads the render counter. // // "At most one", not "exactly one": the strip skips popped-out tabs, so zero diff --git a/HellionChat/SelfTests/TypeScaleStep.cs b/HellionChat/SelfTests/TypeScaleStep.cs index 87c8ed3..ad60b6b 100644 --- a/HellionChat/SelfTests/TypeScaleStep.cs +++ b/HellionChat/SelfTests/TypeScaleStep.cs @@ -7,7 +7,7 @@ using HellionChat.Ui.StyleEngine; namespace HellionChat.SelfTests; -// v1.13.0/A7: the type scale has no call site in the message list until block C, +// v1.13.0: the type scale has no call site in the message list until block C, // so without this step block A would end with nothing to look at and two helpers // (TypeScale, BaselineMath) with no caller at all. // diff --git a/HellionChat/SelfTests/UnreadDecisionStep.cs b/HellionChat/SelfTests/UnreadDecisionStep.cs index 82222e9..aaa6639 100644 --- a/HellionChat/SelfTests/UnreadDecisionStep.cs +++ b/HellionChat/SelfTests/UnreadDecisionStep.cs @@ -3,12 +3,12 @@ using Dalamud.Plugin.SelfTest; namespace HellionChat.SelfTests; -// F3: the unread decision (MessageManager.ShouldCountUnread). Unseen suppresses +// The unread decision (MessageManager.ShouldCountUnread). Unseen suppresses // unread on an inactive tab only when the active tab ALSO shows the message (you -// saw it there) — 1.5.6/upstream semantics, now measured against the REAL active -// tab thanks to F2. Asserts the truth table: suppressed when active tab also -// matches; counts when it does not (the Carla/Jin case); All always counts; None -// counts at the increment layer (the display gate hides it). +// saw it there) -- 1.5.6/upstream semantics, measured against the real active +// tab. Asserts the truth table: suppressed when the active tab also matches; +// counts when it does not; All always counts; None counts at the increment +// layer (the display gate hides it). internal sealed class UnreadDecisionStep : ISelfTestStep { public string Name => "Hellion Chat - Unread decision (per active tab)"; @@ -27,7 +27,8 @@ internal sealed class UnreadDecisionStep : ISelfTestStep } // (b) inactive Unseen tab + the active tab does NOT show the message - // (currentTabMatches=false) => counts (badge). The Carla/Jin case. + // (currentTabMatches=false) => counts (badge). Two people talking in + // a channel the active tab does not carry. if (!MessageManager.ShouldCountUnread(inactive, active, currentTabMatches: false)) { ImGui.Text("(b) inactive Unseen tab must count when the active tab does not show it"); diff --git a/HellionChat/SelfTests/WizardStateSmokeStep.cs b/HellionChat/SelfTests/WizardStateSmokeStep.cs index 61ba2b9..cee3690 100644 --- a/HellionChat/SelfTests/WizardStateSmokeStep.cs +++ b/HellionChat/SelfTests/WizardStateSmokeStep.cs @@ -64,7 +64,7 @@ internal sealed class WizardStateSmokeStep : ISelfTestStep // jumps straight to Step 4 (no Step-3 entry → no seed for // FilterIncludePreviousSessions), commits, and asserts the history // toggle remained on its pre-test value. Pins the null-semantics - // from Spec Z.176 so a regression in CommitPending that started + // so a regression in CommitPending that started // writing seeded recommendations unconditionally would surface // here. // CommitPending → ApplyRoleplay overwrites six privacy / diff --git a/HellionChat/Services/TellRouterService.cs b/HellionChat/Services/TellRouterService.cs index 4133c86..264b81b 100644 --- a/HellionChat/Services/TellRouterService.cs +++ b/HellionChat/Services/TellRouterService.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.Logging; namespace HellionChat.Services; // Routes an incoming tell to the configured TellAutoOpenMode (Off/Sidebar/ -// TopTab/Popout). Decoupled from AutoTellTabsService (Flo decision 2026-06-15): +// TopTab/Popout). Deliberately decoupled from AutoTellTabsService: // that service owns tab CREATION + lifecycle; this only REVEALS/pops the tab it // finds. Popout guards on pool.IsOpen so it never double-pops a tab the // AutoTellTabsOpenAsPopout path already opened. Subscribes to the resolved diff --git a/HellionChat/Themes/Builtin/EventHorizon.cs b/HellionChat/Themes/Builtin/EventHorizon.cs index 40f722b..f226ebc 100644 --- a/HellionChat/Themes/Builtin/EventHorizon.cs +++ b/HellionChat/Themes/Builtin/EventHorizon.cs @@ -51,9 +51,9 @@ internal static class EventHorizon 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. + // Cosmic purple drift: the pale pastels take a lavender tint and + // the accent channels (tell) pull towards magenta-violet. Channel + // identity stays readable throughout. [HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#E6E0F5"), [HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F2C25C"), [HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FF9050"), diff --git a/HellionChat/Themes/Builtin/ForgeMerchantman.cs b/HellionChat/Themes/Builtin/ForgeMerchantman.cs index 030c7ff..56311e2 100644 --- a/HellionChat/Themes/Builtin/ForgeMerchantman.cs +++ b/HellionChat/Themes/Builtin/ForgeMerchantman.cs @@ -51,8 +51,8 @@ internal static class ForgeMerchantman ChatColors: new ThemeChatColors( new Dictionary { - // Forge Merchantman — Patina-Tinte in Party/FC, Bernstein-Tinte in - // Yell/Alliance/CustomEmote. Channel-identity bleibt voll erhalten. + // Patina tint on party and free company, amber on yell, alliance + // and custom emotes. Channel identity is left fully intact. [HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#FFFFFF"), [HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F0C060"), [HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#E8902C"), diff --git a/HellionChat/Themes/Builtin/HellionArctic.cs b/HellionChat/Themes/Builtin/HellionArctic.cs index 0b8ae08..cd284c8 100644 --- a/HellionChat/Themes/Builtin/HellionArctic.cs +++ b/HellionChat/Themes/Builtin/HellionArctic.cs @@ -51,8 +51,8 @@ internal static class HellionArctic ChatColors: new ThemeChatColors( new Dictionary { - // Hellion Arctic — FFXIV-Standard mit dezenter Cyan-Tinte in den - // blauen Channels (Party/FC). Channel-Identität bleibt klar. + // The FFXIV defaults with a restrained cyan tint on the blue + // channels (party, free company). Channel identity stays clear. [HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#FFFFFF"), [HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#FFE066"), [HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FFA040"), diff --git a/HellionChat/Themes/Builtin/IndigoViolet.cs b/HellionChat/Themes/Builtin/IndigoViolet.cs index 7217d27..8897246 100644 --- a/HellionChat/Themes/Builtin/IndigoViolet.cs +++ b/HellionChat/Themes/Builtin/IndigoViolet.cs @@ -51,9 +51,10 @@ internal static class IndigoViolet ChatColors: new ThemeChatColors( new Dictionary { - // Indigo Violet — Lavender-Pink-Drift in Tell und LS6/7. Türkis- - // Mint-Aurora-Counter in Party/FC und LS4. Glitter-Gold in Yell. - // Differenzierung zu Event Horizon: dunkler, dichter, Türkis statt Gold. + // Lavender-pink drift on tell and linkshells 6 and 7, countered by + // a turquoise-mint aurora on party, free company and linkshell 4. + // Glitter gold on yell. What sets it apart from Event Horizon: + // darker, denser, and turquoise where that one goes gold. [HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#F0E6FF"), [HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F0D880"), [HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#F09A60"), diff --git a/HellionChat/Themes/Builtin/MintGrove.cs b/HellionChat/Themes/Builtin/MintGrove.cs index 5caf0c6..fe075dc 100644 --- a/HellionChat/Themes/Builtin/MintGrove.cs +++ b/HellionChat/Themes/Builtin/MintGrove.cs @@ -51,9 +51,9 @@ internal static class MintGrove 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. + // Nature-themed tint: honey amber across the yell family, a mint + // drift in novice network and linkshell. Tell keeps its pink so + // the channel stays recognisable. [HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#E8F5EA"), [HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F9D580"), [HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#F0A050"), diff --git a/HellionChat/Themes/Builtin/NightBlue.cs b/HellionChat/Themes/Builtin/NightBlue.cs index 42b3904..1345c58 100644 --- a/HellionChat/Themes/Builtin/NightBlue.cs +++ b/HellionChat/Themes/Builtin/NightBlue.cs @@ -51,8 +51,9 @@ internal static class NightBlue ChatColors: new ThemeChatColors( new Dictionary { - // Night Blue — Royal-Blue-Tinte in Party/FC, Bronze-Gold in Yell/ - // Alliance. Channel-identity (Tell-Pink, NN-Lime) bleibt erhalten. + // Royal blue on party and free company, bronze gold on yell and + // alliance. Channel identity is preserved -- tell stays pink, + // novice network stays lime. [HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#FFFFFF"), [HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#FFD060"), [HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FFA040"), diff --git a/HellionChat/Themes/Builtin/SynthwaveSunset.cs b/HellionChat/Themes/Builtin/SynthwaveSunset.cs index 6fa0492..0dffa5a 100644 --- a/HellionChat/Themes/Builtin/SynthwaveSunset.cs +++ b/HellionChat/Themes/Builtin/SynthwaveSunset.cs @@ -51,8 +51,9 @@ internal static class SynthwaveSunset ChatColors: new ThemeChatColors( new Dictionary { - // Synthwave Sunset — Magenta dominiert die warmen Channels (Yell/Shout/FC), - // Cyan dominiert die kühlen (Tell/Party). Neon-Akzente für Status-nahe Channels. + // Magenta carries the warm channels (yell, shout, free company), + // cyan the cool ones (tell, party). Neon accents on the status- + // adjacent channels. [HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#F0DFFF"), [HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#FF2D95"), [HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FF6BB6"), diff --git a/HellionChat/Themes/ThemeRegistry.cs b/HellionChat/Themes/ThemeRegistry.cs index 8f063bd..22bbd25 100644 --- a/HellionChat/Themes/ThemeRegistry.cs +++ b/HellionChat/Themes/ThemeRegistry.cs @@ -10,7 +10,7 @@ public sealed class ThemeRegistry public const string DefaultSlug = HellionArctic.Slug; - // 1Hz throttle for the v1.4.8 B2 auto-refresh-on-active path. The + // 1Hz throttle for the v1.4.8 auto-refresh-on-active path. The // Plugin.Draw hook calls RefreshActiveIfStale every frame, but the // actual File.GetLastWriteTimeUtc disk-stat only runs once per second // -- 60fps would otherwise mean 3600 stats/min on the same path (more @@ -24,7 +24,7 @@ public sealed class ThemeRegistry private readonly string? _customThemesDir; private Theme _active; - // v1.4.8 B2: source path of the currently active custom theme. Captured + // v1.4.8: source path of the currently active custom theme. Captured // at Switch() time so RefreshActiveIfStale does not have to reconstruct // a filename from the slug -- custom theme filenames are not required // to match the slug they declare in the JSON body. Null when the active @@ -33,7 +33,7 @@ public sealed class ThemeRegistry private long _lastActiveStampCheckMs = -ActiveStampPollIntervalMs; private DateTime _lastActiveStamp = DateTime.MinValue; - // PM-1 crossfade state. Switch() captures the previous AbgrCache as a + // crossfade state. Switch() captures the previous AbgrCache as a // VALUE-COPY (not a Theme reference) -- the built-in singletons share // their RecomputeAbgrCache identity, so a reference would mutate // alongside the new active. _crossfadeStartTickMs == long.MinValue @@ -54,7 +54,7 @@ public sealed class ThemeRegistry internal void SetActiveChangedCallback(Action callback) => _onActiveChanged = callback; // Shared slug guard for any code path that turns a slug into a filename. - // Both SaveEditingBuffer (F1) and ImportFromPath (M6) call this so the + // Both SaveEditingBuffer and ImportFromPath call this so the // path-traversal/invalid-char rules live in exactly one place. // // Whitespace rejection is intentional: Path.GetInvalidFileNameChars on @@ -112,16 +112,16 @@ public sealed class ThemeRegistry public Theme Active => _active; // Read-only exposure of the configured custom themes directory. - // M6 ThemeImportExportRow opens this path via Process.Start. + // ThemeImportExportRow opens this path via Process.Start. public string? CustomThemesDir => _customThemesDir; - // Read-only enumeration of all built-in theme slugs. T2 ThemePickerCategoryStep + // Read-only enumeration of all built-in theme slugs. ThemePickerCategoryStep // diffs this set against ThemePicker.CategoryMapSlugs to enforce coverage. public IEnumerable BuiltinSlugs => _builtIns.Keys; // True try-pattern lookup: returns false when neither built-in nor custom - // cache holds the slug, no fallback to default. M3 ThemePicker uses this - // for card-rendering, M6 ThemeImportExportRow for fork-slug collisions. + // cache holds the slug, no fallback to default. ThemePicker uses this + // for card-rendering, ThemeImportExportRow for fork-slug collisions. // Cold-cache fallback: see `LoadCustomBySlug` lookup-by-slug reverse // iteration — it only walks the pre-populated _customCache. If a freshly // imported file has not been enumerated yet (or no warm-up ran), the first @@ -292,8 +292,8 @@ public sealed class ThemeRegistry // Switch() prefers built-ins over custom themes with the same slug // (see `Switch` built-in-first lookup), so saving a custom file under // a built-in slug persists the file but leaves the built-in active — - // looks green, behaves broken. M4 ColorPicker DrawIdleState forks - // built-in themes into a custom slug before BeginEditing, M6 + // looks green, behaves broken. ColorPicker DrawIdleState forks + // built-in themes into a custom slug before BeginEditing, // ImportFromPath renames built-in-colliding imports to _imported. // New call-sites must either fork first or rename to a non-built-in slug. public bool SaveEditingBuffer(out string targetPath) @@ -308,7 +308,7 @@ public sealed class ThemeRegistry // separators, parent-directory tokens, or platform-invalid filename chars. // Without this guard an imported theme with Slug "../../../etc/passwd" // would let Path.Combine escape _customThemesDir entirely. Shared helper - // so M6 ImportFromPath uses the exact same rule set. + // so ImportFromPath uses the exact same rule set. var safeSlug = _editingThemeBuffer.Slug; if (!IsSafeThemeSlug(safeSlug)) { @@ -326,8 +326,8 @@ public sealed class ThemeRegistry // persist a custom file under a built-in slug — the file lands on disk, // Switch keeps the built-in active, and the post-save active-slug check // below returns false. The caller then sees "save failed" while a garbage - // file accumulates in the themes dir on every retry. M4 ColorPicker forks - // built-in themes into a custom slug before BeginEditing, M6 ImportFromPath + // file accumulates in the themes dir on every retry. ColorPicker forks + // built-in themes into a custom slug before BeginEditing, ImportFromPath // renames built-in-colliding imports to _imported, so production // paths already steer clear; this guard catches everything else. if (_builtIns.ContainsKey(safeSlug)) @@ -522,8 +522,8 @@ public sealed class ThemeRegistry ) { var t = (float)(now - _crossfadeStartTickMs) / CrossfadeDurationMs; - // A2: SmoothStep easing so the fade eases in/out instead of a - // linear ramp. MUST stay in lockstep with TryGetActiveCrossfade (K8). + // SmoothStep easing so the fade eases in and out instead of running + // linear. MUST stay in lockstep with TryGetActiveCrossfade. var te = t * t * (3f - 2f * t); snapshot = ThemeAbgrCacheLerp.Lerp(_previousAbgrSnapshot.Value, _active.AbgrCache, te); } @@ -551,7 +551,7 @@ public sealed class ThemeRegistry return false; var t = (float)elapsed / CrossfadeDurationMs; - // A2: SmoothStep easing -- keep identical to ArmCrossfade (K8). + // SmoothStep easing -- keep identical to ArmCrossfade. var te = t * t * (3f - 2f * t); lerped = ThemeAbgrCacheLerp.Lerp(_previousAbgrSnapshot.Value, _active.AbgrCache, te); return true; diff --git a/HellionChat/Themes/ThemeStampDiff.cs b/HellionChat/Themes/ThemeStampDiff.cs index aa59a2d..3e1a575 100644 --- a/HellionChat/Themes/ThemeStampDiff.cs +++ b/HellionChat/Themes/ThemeStampDiff.cs @@ -1,6 +1,6 @@ namespace HellionChat.Themes; -// Pure stale-check for the v1.4.8 B2 theme-auto-refresh-on-active path. +// Pure stale-check for the v1.4.8 theme-auto-refresh-on-active path. // Lives in a free helper class so the Build-Suite can exercise the diff // rules without instantiating ThemeRegistry (which touches the Dalamud // log proxy and the filesystem). The rules: diff --git a/HellionChat/Ui/CommandHelpWindow.cs b/HellionChat/Ui/CommandHelpWindow.cs index 0d4caa4..a9831ad 100644 --- a/HellionChat/Ui/CommandHelpWindow.cs +++ b/HellionChat/Ui/CommandHelpWindow.cs @@ -18,7 +18,7 @@ internal sealed class CommandHelpWindow : Window // Setter-injected post-ctor to break the InputBar -> CommandHelpWindow -> // MainWindow -> InputBar singleton cycle (MS.DI does not detect cycles // through FactoryCallSite registrations). Wired in - // CommandHelpWindowInitHostedService.StartAsync, same §6.2 pattern as + // CommandHelpWindowInitHostedService.StartAsync, same setter-injection pattern as // MessageList.AttachPayloadHandler. private Windows.MainWindow? _mainWindow; @@ -41,7 +41,7 @@ internal sealed class CommandHelpWindow : Window RespectCloseHotkey = false; DisableWindowSounds = true; - // Logger injected for future diagnostic hooks (no call-sites yet in R2). + // Logger injected for future diagnostic hooks; no call sites yet. _ = _logger; } diff --git a/HellionChat/Ui/Components/CardClipPlanner.cs b/HellionChat/Ui/Components/CardClipPlanner.cs index ea0db1b..c6b0920 100644 --- a/HellionChat/Ui/Components/CardClipPlanner.cs +++ b/HellionChat/Ui/Components/CardClipPlanner.cs @@ -2,8 +2,8 @@ using System.Collections.Generic; namespace HellionChat.Ui.Components; -// B2 (PERF-B2): variable-height clip plan. ImGuiListClipper needs a constant -// row height, and since v1.10.0/A2 neither density has one (compact rows wrap +// Variable-height clip plan. ImGuiListClipper needs a constant +// row height, and since v1.10.0 neither density has one (compact rows wrap // too), so both compute a plan from the cached per-row heights: a lead dummy // for the rows above // the viewport, the [first..last] index range that overlaps the viewport, and diff --git a/HellionChat/Ui/Components/ChunkRenderer.cs b/HellionChat/Ui/Components/ChunkRenderer.cs index 5439523..2c94df1 100644 --- a/HellionChat/Ui/Components/ChunkRenderer.cs +++ b/HellionChat/Ui/Components/ChunkRenderer.cs @@ -34,11 +34,11 @@ internal sealed class ChunkRenderer // names change every plugin reload to avoid stable cross-session linkage. _salt = new Random().Next().ToString(); - // Not yet consumed in C2/C3; E-task wiring will likely add log call-sites later. + // No call sites yet; logging here will likely come later. _ = _logger; } - // B2-1/B2-2 render-observability: the formatted sender text the real draw + // render-observability: the formatted sender text the real draw // path actually produced (post-ForDisplay). A SelfTest reads this after // driving DrawChunks to prove the WorldSuffixMode/NameFormMode reformat // reached the real render entry — never the helper in isolation. null until @@ -52,7 +52,7 @@ internal sealed class ChunkRenderer float lineWidth = 0f ) { - // UI-7: render a copy with the sender name reformatted per the user's + // Render a copy with the sender name reformatted per the user's // display options. Skipped in screenshot mode so the name-anonymising // path in DrawChunk stays reliable (privacy wins). ForDisplay returns // the list unchanged when nothing applies, so non-sender lists and the diff --git a/HellionChat/Ui/Components/InputBar.cs b/HellionChat/Ui/Components/InputBar.cs index 2e18f96..ebf00a1 100644 --- a/HellionChat/Ui/Components/InputBar.cs +++ b/HellionChat/Ui/Components/InputBar.cs @@ -64,7 +64,7 @@ internal sealed class InputBar private bool _wasInputTextHovered; private bool? _isFocusedOverride; // Test-only; null = honour per-frame Draw() value. - // UI-11 plugin-disclosure arm-and-hold: holds the buffer that armed the + // plugin-disclosure arm-and-hold: holds the buffer that armed the // disclosure warning. null = not armed. Compared by value so an edit // re-arms and a resend on the identical buffer goes through. 1.5.6 parity // (ChatInputBar 1d3b429:27). @@ -118,7 +118,7 @@ internal sealed class InputBar // Note: when MainWindow is closed, DrawInputField never runs, so // _isFocused keeps the last value written by the previous draw pass. // The consumer that actually pushes this state across the IPC boundary - // (TypingIpc.BuildState, see F3 Step 2) gates on Plugin.MainWindow.IsOpen + // (TypingIpc.BuildState) gates on Plugin.MainWindow.IsOpen // itself, so the stale backing-field never leaks to subscribers. Mirroring // the gate here would require an extra Plugin-backref in InputBar that the // rest of the component doesn't need. @@ -196,7 +196,7 @@ internal sealed class InputBar ImGui.SameLine(); DrawQuickButtons(); - // UI-11: yellow inline warning while a plugin-only-glyph message is + // Yellow inline warning while a plugin-only-glyph message is // armed-and-held (buffer unchanged since it armed). Renders on its own // line below the input row. 1.5.6 parity (ChatInputBar 1d3b429:93-103). if ( @@ -589,7 +589,7 @@ internal sealed class InputBar if (string.IsNullOrEmpty(text)) return; - // UI-11: plugin-disclosure arm-and-hold. Arm + scan on the RAW + // Plugin-disclosure arm-and-hold. Arm + scan on the RAW // _pendingMessage (NOT the trimmed `text`) so the Draw warning gate // (_pendingMessage == _disclosureArmedBuffer) matches byte-for-byte even // when the buffer has leading/trailing whitespace. 1.5.6 armed/held/ @@ -1183,7 +1183,7 @@ internal sealed class InputBar } // DTO for an in-flight auto-translate completion. Lives as a companion type -// in this file because it is only consumed by InputBar (see v1.7.1 Fix #4 plan §2.4). +// in this file because it is only consumed by InputBar. internal sealed class AutoCompleteInfo { // ToComplete MUST be a mutable field (not an auto-property), because the diff --git a/HellionChat/Ui/Components/MessageList.cs b/HellionChat/Ui/Components/MessageList.cs index f11d73e..4704632 100644 --- a/HellionChat/Ui/Components/MessageList.cs +++ b/HellionChat/Ui/Components/MessageList.cs @@ -18,13 +18,13 @@ internal sealed class MessageList private PayloadHandler? _handler; - // B3-5: scroll-to-bottom state. Per-instance, so pop-out windows (own + // Scroll-to-bottom state. Per-instance, so pop-out windows (own // MessageList instance, PluginHostFactory.cs:263-266) isolate automatically — // the old 1.5.6 updateScrollState flag is NOT needed here. private bool _scrolledUp; private bool _scrollToBottomRequested; - // B2: the height cache is only valid while these inputs are unchanged. + // The height cache is only valid while these inputs are unchanged. // FontManager's own fingerprint covers font sizes only, not density / the two // name-display modes / width — a stale height would misplace the clipper dummies. // Per tab, not per list: the old single field let a width change in tab A mark @@ -38,7 +38,7 @@ internal sealed class MessageList private readonly Action _drawCardRow; // Reused across frames: at MessageManager.MessageDisplayLimit a fresh array - // per frame is 40 KB of garbage, and A2 put the default density on this + // per frame is 40 KB of garbage, and A later cycle put the default density on this // path. The old comment named MaxLinesToRender and its 2500 default, a // config field that had stopped bounding anything. private float[] _heightScratch = []; @@ -49,7 +49,7 @@ internal sealed class MessageList private bool _stampVisible; private float _metaDrop; - // §6.2: setter-injection breaks the PayloadHandler → MainWindow → MessageList → PayloadHandler 3-cycle. + // Setter-injection breaks the PayloadHandler → MainWindow → MessageList → PayloadHandler 3-cycle. // Wired by PayloadHandlerInitHostedService.StartAsync after both singletons exist. internal void AttachPayloadHandler(PayloadHandler handler) { @@ -73,12 +73,12 @@ internal sealed class MessageList return snap; } - // SelfTest hook (B3-5 reset-invariant, REQUIRED — not optional). Lets + // SelfTest hook (reset-invariant, REQUIRED — not optional). Lets // ScrollSnapDecisionStep flip the request flag without a real click, so the // post-snap reset can be asserted; without it only the OR branch is testable. internal void RequestScrollToBottomForSelfTest() => _scrollToBottomRequested = true; - // SelfTest hook (B2): runs the real planner against a caller fixture so the + // SelfTest hook: runs the real planner against a caller fixture so the // step asserts the plan without a live scroll child (GetScrollY is garbage headless). internal CardClipPlan PlanCardClipForSelfTest( IReadOnlyList heights, @@ -86,9 +86,9 @@ internal sealed class MessageList float viewportHeight ) => CardClipPlanner.Plan(heights, scrollY, viewportHeight); - // SelfTest hook (B2): drives the live invalidation, returns the tab's remaining + // SelfTest hook: drives the live invalidation, returns the tab's remaining // cached-height count so the step can assert the drop. nowMs is a parameter so - // the step can step past the settle window without sleeping (v1.10.0/A1). + // the step can step past the settle window without sleeping (v1.10.0). internal int RunHeightCacheInvalidationForSelfTest(Tab tab, float contentWidth, long nowMs) { InvalidateHeightCacheIfLayoutChanged(tab, contentWidth, nowMs); @@ -165,7 +165,7 @@ internal sealed class MessageList MeasureTimestampColumn(tab); - // B2: drop stale cached heights before the snapshot draw. Both densities + // Drop stale cached heights before the snapshot draw. Both densities // need this now -- compact rows are not constant height either, they wrap. // Width read here while it is valid. InvalidateHeightCacheIfLayoutChanged( @@ -187,7 +187,7 @@ internal sealed class MessageList var frozen = _fingerprintGates[tab.Identifier].IsPending; DrawRows(tab, messages, compact ? _drawCompactRow : _drawCardRow, frozen); - // B3-5: scroll values are frame-constant inside the child, so this + // Scroll values are frame-constant inside the child, so this // reflects the current frame's state wherever it runs; kept after the // render to mirror the 1.5.6 end-of-DrawMessageLog placement. _scrolledUp = ImGui.GetScrollMaxY() - ImGui.GetScrollY() > 1f; @@ -282,7 +282,7 @@ internal sealed class MessageList ImGui.SetCursorPos(origin with { X = origin.X + _stampColumnWidth }); } - // B3-5: Discord-style full-width bar pinned to the bottom edge of the + // Discord-style full-width bar pinned to the bottom edge of the // visible region while the user is scrolled up. Geometry comes from window // pos + size (visible region), never from the content flow: when scrolled // up the visible bottom sits above the content bottom, so the @@ -335,7 +335,7 @@ internal sealed class MessageList private void DrawCompactRow(Message message, string? previousStamp) { - // B2-1/B2-2: render the sender through DrawChunks (the name-aware path + // Render the sender through DrawChunks (the name-aware path // that applies WorldSuffixMode/NameFormMode via ForDisplay), not as a // flat SenderSource.TextValue string. message.Sender already carries the // channel brackets/colon as ChunkSource.None wrappers (MessageManager @@ -566,10 +566,10 @@ internal sealed class MessageList private void DrawCardRow(Message message, string? previousStamp) { - // B2-1/B2-2: sender via DrawChunks (name-aware path), on its own line + // Sender via DrawChunks (name-aware path), on its own line // with content below — 1.5.6 card parity (ChatLogWindow.cs:1913, no // SameLine after the sender). The 1.5.6 channel-colour push on the - // sender is deferred styling polish (masterplan §6 -> v1.9.0); plain + // sender is deferred styling polish (deferred to v1.9.0); plain // text here. // A system message has no sender, so a header row would be a stamp on a // line of its own -- an empty gesture. Those stay single-line in both diff --git a/HellionChat/Ui/Components/Settings/LivePreviewPanel.cs b/HellionChat/Ui/Components/Settings/LivePreviewPanel.cs index 32cf0a6..4e47825 100644 --- a/HellionChat/Ui/Components/Settings/LivePreviewPanel.cs +++ b/HellionChat/Ui/Components/Settings/LivePreviewPanel.cs @@ -20,7 +20,7 @@ internal sealed class LivePreviewPanel : IDisposable private static uint Abgr(StyleEngine.Token token, ThemeColors colors) => ColourUtil.RgbaToAbgr(Tokens.Resolve(token, colors)); - // Static counter for S5 reload-stress verification: after 10 reloads the + // Static counter for reload-stress verification: after 10 reloads the // counter must read 0 (plugin disabled) or 1 (plugin enabled). Anything // higher signals a Dispose skip and a subscriber leak against ThemeRegistry. internal static int InstanceCount; diff --git a/HellionChat/Ui/Components/Settings/Tabs/AboutTab.cs b/HellionChat/Ui/Components/Settings/Tabs/AboutTab.cs index 8d4e1b8..f359650 100644 --- a/HellionChat/Ui/Components/Settings/Tabs/AboutTab.cs +++ b/HellionChat/Ui/Components/Settings/Tabs/AboutTab.cs @@ -133,7 +133,7 @@ internal sealed class AboutTab LastHonorificStatusKey = kind.ToString(); var colors = _themes.Active.Colors; - // Null-safety via the `is { } v` pattern, never `.Value` raw (spec SEC-2): + // Null-safety via the `is { } v` pattern, never `.Value` raw : // the version is bound only on the arms that have it; the impossible // Detected/Incompatible-without-version state falls through to default. switch (kind) diff --git a/HellionChat/Ui/Components/Settings/Tabs/DataPrivacyTab.cs b/HellionChat/Ui/Components/Settings/Tabs/DataPrivacyTab.cs index 8bf389f..3b94650 100644 --- a/HellionChat/Ui/Components/Settings/Tabs/DataPrivacyTab.cs +++ b/HellionChat/Ui/Components/Settings/Tabs/DataPrivacyTab.cs @@ -254,7 +254,7 @@ internal sealed class DataPrivacyTab ) { // Read-only statement, not a switch. Do not promote it to one - // without an explicit Sub-Spec change: a toggle implies there is + // without an explicit design change: a toggle implies there is // something to turn off. ImGuiUtil.HelpText(HellionStrings.Settings_Telemetry_None); } diff --git a/HellionChat/Ui/Components/Settings/ThemeImportExportRow.cs b/HellionChat/Ui/Components/Settings/ThemeImportExportRow.cs index 2801923..853c7c4 100644 --- a/HellionChat/Ui/Components/Settings/ThemeImportExportRow.cs +++ b/HellionChat/Ui/Components/Settings/ThemeImportExportRow.cs @@ -173,8 +173,7 @@ internal sealed class ThemeImportExportRow // Slug sanitisation BEFORE BeginEditing — SaveEditingBuffer would // reject too, but rejecting here means an unsafe slug never enters // the editing buffer. Shared helper ThemeRegistry.IsSafeThemeSlug - // keeps the rule set in sync with F1's save-side guard (see - // ThemeRegistry.IsSafeThemeSlug shared helper). + // keeps the rule set in sync with the save-side guard. var importSlug = theme.Slug; if (!ThemeRegistry.IsSafeThemeSlug(importSlug)) { @@ -186,9 +185,9 @@ internal sealed class ThemeImportExportRow return; } - // Pragmatic deviation from §1.6 wording ("File.Copy into themes/"): - // BeginEditing+SaveEditingBuffer produces the same end-state and - // reuses the validated F1 save pipeline. Trade-off: destination + // Not a plain File.Copy into themes/: BeginEditing+SaveEditingBuffer + // produces the same end state and reuses the validated save + // pipeline. Trade-off: destination // filename becomes the theme's slug, not the original filename. // // Slug-collision handling: diff --git a/HellionChat/Ui/Components/Settings/ThemePicker.cs b/HellionChat/Ui/Components/Settings/ThemePicker.cs index 6069633..8ae6da0 100644 --- a/HellionChat/Ui/Components/Settings/ThemePicker.cs +++ b/HellionChat/Ui/Components/Settings/ThemePicker.cs @@ -29,7 +29,7 @@ internal sealed class ThemePicker (HellionStrings.Settings_Theme_Category_Retro, new[] { "synthwave-sunset" }, false), ]; - // T2 ThemePickerCategoryStep diffs this against ThemeRegistry.BuiltinSlugs + // ThemePickerCategoryStep diffs this against ThemeRegistry.BuiltinSlugs // to enforce coverage. Kept on the static map so the test does not pierce instance state. internal static IEnumerable CategoryMapSlugs => CategoryMap.SelectMany(c => c.Slugs); diff --git a/HellionChat/Ui/Components/Sidebar.cs b/HellionChat/Ui/Components/Sidebar.cs index 4adad40..bd5be26 100644 --- a/HellionChat/Ui/Components/Sidebar.cs +++ b/HellionChat/Ui/Components/Sidebar.cs @@ -22,10 +22,14 @@ internal sealed class Sidebar { public const float IconOnlyWidth = 38f; - // B1-3a: expanded sidebar width is user-configurable (Config.SidebarWidth), - // clamped to these bounds (matches the ChannelsTab slider range). Replaces - // the old fixed 150px ExpandedWidth constant. - public const float MinSidebarWidth = 40f; + // Expanded sidebar width is user-configurable (Config.SidebarWidth), + // clamped to these bounds (matches the ChannelsTab slider range). + // + // The floor is where a tab name stops being readable, not where the icons + // stop fitting: 40 let the expanded sidebar be narrower than the icon-only + // one, which drew labels into a column too narrow to hold them. Anyone who + // wants it that slim wants the collapsed layout, and that is IconOnlyWidth. + public const float MinSidebarWidth = 130f; public const float MaxSidebarWidth = 300f; private static float RowHeight => Metrics.SidebarRowHeight; @@ -38,7 +42,7 @@ internal sealed class Sidebar // previously active one -- that row was still active when it was painted. internal int LastRenderedActiveSurfaceCount { get; private set; } - // B3-2 render observability: counts greeted glyphs actually drawn this frame. + // render observability: counts greeted glyphs actually drawn this frame. // Incremented ONLY in the real glyph branch in DrawRow; reset at Draw start. // The SelfTest reads it after driving the real Draw — no dead service roundtrip. internal int LastRenderedGreetedGlyphCount; @@ -48,7 +52,7 @@ internal sealed class Sidebar // beside it. private const float PinGlyphScale = 0.6f; - // B3-4 render observability: section headers actually drawn this frame. + // render observability: section headers actually drawn this frame. // Incremented only in the real header branch; reset at Draw start. internal int LastDrawnSectionHeaderCount; @@ -149,7 +153,7 @@ internal sealed class Sidebar var dimAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextDim); var dl = ImGui.GetWindowDrawList(); - // B3-4 sectioned render order (1.5.6 parity): persistent → pinned + // sectioned render order (1.5.6 parity): persistent → pinned // TempTabs → unpinned TempTabs. Only the display sequence regroups; // the tab list itself stays untouched and every row keeps its // ORIGINAL list index for PushID, so an open context-menu popup @@ -250,7 +254,7 @@ internal sealed class Sidebar // Only split off a separate pop-out hit area when there's room for // both buttons. Below that, the whole row stays as a single // selectable strip without the pop-out affordance. - // A3: gate the pop-out affordance on the expanded sidebar too. In + // Gate the pop-out affordance on the expanded sidebar too. In // icon-only mode avail still clears the width threshold, which used to // paint the pop-out glyph over the tab icon. The row stays a single // selectable strip when collapsed; right-click pop-out is unaffected. @@ -476,7 +480,7 @@ internal sealed class Sidebar // The hit area sits at the LEFT edge of the row, but the item must // be submitted AFTER TabContextMenu.Draw — any interactive item // between the row button and the popup call would steal the - // right-click trigger (B3-1 ordering constraint). + // right-click trigger (ordering constraint). ImGui.SetCursorScreenPos(origin); // CheckCircle = greeted, plain Check = still pending (1.5.6 mapping). diff --git a/HellionChat/Ui/Components/TabContextMenu.cs b/HellionChat/Ui/Components/TabContextMenu.cs index 10ffa80..5cece2c 100644 --- a/HellionChat/Ui/Components/TabContextMenu.cs +++ b/HellionChat/Ui/Components/TabContextMenu.cs @@ -82,7 +82,7 @@ internal static class TabContextMenu ClearPendingRename(); } - // Per-tab notification sound (B3-3). The checkbox gates the picker so + // Per-tab notification sound. The checkbox gates the picker so // tabs that never want a sound keep the popup short. if ( ImGui.Checkbox( diff --git a/HellionChat/Ui/DbViewer.cs b/HellionChat/Ui/DbViewer.cs index b539f72..35cce15 100644 --- a/HellionChat/Ui/DbViewer.cs +++ b/HellionChat/Ui/DbViewer.cs @@ -36,7 +36,7 @@ public class DbViewer : Window private int CurrentPage = 1; private string SimpleSearchTerm = ""; - // v1.4.8 H2: opt-in full-text search across the whole DB via FTS5. + // v1.4.8: opt-in full-text search across the whole DB via FTS5. // Transient UI state (per-session), not persisted -- users opt in fresh // every time so they always see the page-filter as the default mode. private bool UseFullTextSearch; @@ -232,7 +232,7 @@ public class DbViewer : Window tooltipRight: Language.Page_ArrowRight_Tooltip ); - // Full-text search toggle (v1.4.8 H2). IsFtsIndexBuilt is a cached + // Full-text search toggle (v1.4.8). IsFtsIndexBuilt is a cached // volatile bool in MessageStore -- single field read per frame, no // SELECT count(*). ImRaii.Disabled blocks any click while the index // is still being built, so no defensive force-off branch needed diff --git a/HellionChat/Ui/FirstRunWizard.cs b/HellionChat/Ui/FirstRunWizard.cs index ea2017d..d8fa371 100644 --- a/HellionChat/Ui/FirstRunWizard.cs +++ b/HellionChat/Ui/FirstRunWizard.cs @@ -1,6 +1,7 @@ using System.Globalization; using System.Numerics; using Dalamud.Bindings.ImGui; +using Dalamud.Interface; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; @@ -9,6 +10,7 @@ using HellionChat.Code; using HellionChat.Privacy; using HellionChat.Resources; using HellionChat.Themes; +using HellionChat.Ui.StyleEngine; using HellionChat.Util; namespace HellionChat.Ui; @@ -19,30 +21,42 @@ namespace HellionChat.Ui; // nested WizardState record; every step writes nullable Pending* // fields, and CommitPending() applies only the non-null ones so // users who skip a step never get their existing config overwritten. +// +// This is the first thing a tester ever sees, and until v2.0.0 it was the last +// surface still drawn in ImGui defaults -- stock buttons under a hardcoded +// bronze that ignored the theme. It speaks the window's language now: the +// settings backdrop, tracked-caps headings, the chamfered accent pill for the +// one action that matters, and ghosts for the ones that do not. public sealed class FirstRunWizard : Window { - // Forge-Bronze (#C2410C). The same constant lives in ThemeRegistry - // and the forge-announce workflow; pinning it locally keeps the - // wizard render path free of registry lookups during draw. - private static readonly Vector4 ForgeBronze = new(0xC2 / 255f, 0x41 / 255f, 0x0C / 255f, 1f); - private static readonly Vector4 ForgeBronzeDim = new( - 0xC2 / 255f, - 0x41 / 255f, - 0x0C / 255f, - 0.3f - ); - private const int TotalSteps = 4; private readonly Plugin Plugin; + private readonly StyleEngine.SurfaceBackdrop _backdrop; private readonly WizardState _state = new(); - internal FirstRunWizard(Plugin plugin) + // Its own instance rather than the settings window's: TokenResolver is a + // stateless lookup, so a second one costs a field, while a constructor + // parameter would touch PluginHostFactory for nothing. + private readonly Components.Settings.SettingsWidgets _widgets; + + internal FirstRunWizard(Plugin plugin, StyleEngine.SurfaceBackdrop backdrop) : base($"{HellionStrings.Wizard_Title}###hellion-firstrun") { Plugin = plugin; + _backdrop = backdrop; + _widgets = new Components.Settings.SettingsWidgets( + plugin, + new Components.Settings.SettingsPalette(new StyleEngine.TokenResolver()) + ); Flags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking; + + // Opaque, unlike every other window in the plugin. Those are read at a + // glance over the game; this one is read once, carries a privacy + // decision, and the free company list showing through its cards is + // noise the reader has to work past. + BgAlpha = 1f; SizeCondition = ImGuiCond.Appearing; Size = new Vector2(720, 480); SizeConstraints = new WindowSizeConstraints @@ -52,6 +66,12 @@ public sealed class FirstRunWizard : Window }; } + // Until v2.0.0 the wizard could only ever open once per session, so its + // state living as long as the plugin never showed. /hellion wizard makes + // reopening possible, and a second run that started on step 4 with the + // first run's choices still pending would commit picks the user never saw. + public override void OnOpen() => _state.Reset(); + public override void OnClose() { // OnClose fires on explicit X-click and on plugin dispose. We never @@ -63,10 +83,12 @@ public sealed class FirstRunWizard : Window public override void Draw() { + // Same floor as the settings window, full strength: the wizard is read + // in glances, not line by line, and it is the first impression. + _backdrop.Draw(accentWashHeight: 46f, moteIntensity: 1f, strength: 1f, opacityOverride: 1f); + DrawPagination(); ImGui.Spacing(); - ImGui.Separator(); - ImGui.Spacing(); switch (_state.CurrentStep) { @@ -94,26 +116,149 @@ public sealed class FirstRunWizard : Window var draw = ImGui.GetWindowDrawList(); var avail = ImGui.GetContentRegionAvail(); var cursor = ImGui.GetCursorScreenPos(); - const float radius = 5f; - const float spacing = 16f; + var scale = StyleEngine.Metrics.Scale; + var radius = 4f * scale; + var spacing = 18f * scale; var totalWidth = (TotalSteps - 1) * spacing; var startX = cursor.X + avail.X - totalWidth - radius; + // Theme accent, contrast-checked, instead of the hardcoded forge bronze + // the wizard carried since v1.5.2 -- it was the one surface that ignored + // the theme entirely. The current dot is filled, the rest are rings, so + // the step reads by shape as well as by brightness. + var c = Plugin.ThemeRegistry.Active.Colors; + var accent = ColourUtil.EnsureContrast( + ColourUtil.RgbaToAbgr(c.Accent), + ColourUtil.RgbaToAbgr(c.WindowBg), + 3f + ); + var dim = ColourUtil.ApplyAlpha(accent, 0.35f); + for (var i = 0; i < TotalSteps; i++) { - var color = (i + 1) == _state.CurrentStep ? ForgeBronze : ForgeBronzeDim; - var packed = ImGui.GetColorU32(color); - draw.AddCircleFilled( - new Vector2(startX + i * spacing, cursor.Y + radius), - radius, - packed - ); + var centre = new Vector2(startX + i * spacing, cursor.Y + radius); + if ((i + 1) == _state.CurrentStep) + draw.AddCircleFilled(centre, radius, accent); + else + draw.AddCircle(centre, radius - 1f, dim, 0, MathF.Max(1f, scale)); } // Reserve vertical space the circles consumed so the next widget starts below them. ImGui.Dummy(new Vector2(0, radius * 2)); } + // The heading language every finished window speaks: tracked small caps with + // a rule that fades out. Shape, not hue, so it reads in every palette. + // Contrast-checked theme accent as a Vector4, for the text pushes that used + // to hardcode forge bronze. The double swap is deliberate: RgbaToAbgr is an + // involution, so it converts ABGR back to the RGBA that RgbaToVector4 reads. + private Vector4 AccentVec4() + { + var c = Plugin.ThemeRegistry.Active.Colors; + var abgr = ColourUtil.EnsureContrast( + ColourUtil.RgbaToAbgr(c.Accent), + ColourUtil.RgbaToAbgr(c.WindowBg), + 4.5f + ); + return ColourUtil.RgbaToVector4(ColourUtil.RgbaToAbgr(abgr)); + } + + private void DrawHeading(string text) + { + var scale = StyleEngine.Metrics.Scale; + var c = Plugin.ThemeRegistry.Active.Colors; + var dl = ImGui.GetWindowDrawList(); + var pos = ImGui.GetCursorScreenPos(); + + var tint = ColourUtil.EnsureContrast( + ColourUtil.RgbaToAbgr(c.TextMuted), + ColourUtil.RgbaToAbgr(c.WindowBg), + 4.5f + ); + var width = dl.DrawTrackedText(pos, text.ToUpperInvariant(), tint, 1.8f * scale); + + var ruleX = pos.X + width + 10f * scale; + var availX = ImGui.GetContentRegionAvail().X; + if (pos.X + availX > ruleX) + dl.DrawFadeRule( + new Vector2(ruleX, pos.Y + ImGui.GetTextLineHeight() * 0.5f), + pos.X + availX - ruleX, + ColourUtil.RgbaToAbgr(c.Border), + MathF.Max(1f, scale) + ); + + ImGui.Dummy(new Vector2(0f, ImGui.GetTextLineHeight() + 6f * scale)); + } + + // The primary action is the channel pill's shape -- chamfered accent with a + // white depth gradient -- because it is the one thing on the page the user + // is meant to press. Everything secondary is a ghost. + private bool DrawPrimaryPill(string label) + { + var scale = StyleEngine.Metrics.Scale; + var c = Plugin.ThemeRegistry.Active.Colors; + var dl = ImGui.GetWindowDrawList(); + + var size = new Vector2(ImGui.CalcTextSize(label).X + 36f * scale, ImGui.GetFrameHeight()); + var origin = ImGui.GetCursorScreenPos(); + + var clicked = ImGui.InvisibleButton($"##wiz-primary-{label}", size); + var hovered = ImGui.IsItemHovered(); + var amount = StyleEngine.HoverState.Query(ImGui.GetID($"##wiz-primary-{label}"), hovered); + + var fill = ColourUtil.RgbaToAbgr(c.Accent); + if (amount > 0f) + fill = ColourUtil.LerpTowardWhite(fill, amount * 0.12f); + + dl.DrawSlipPolygon(origin, origin + size, ColourUtil.RgbaToAbgr(fill), 6f * scale); + dl.DrawVerticalGradient(origin, origin + size, 0x28FFFFFFu, 0u); + + var ink = ColourUtil.EnsureContrast( + ColourUtil.RgbaToAbgr(c.WindowBg), + ColourUtil.RgbaToAbgr(c.Accent), + 4.5f + ); + var textSize = ImGui.CalcTextSize(label); + dl.AddText(origin + (size - textSize) * 0.5f, ink, label); + + return clicked; + } + + private bool DrawGhostLink(string id, string label) + { + var scale = StyleEngine.Metrics.Scale; + var c = Plugin.ThemeRegistry.Active.Colors; + var dl = ImGui.GetWindowDrawList(); + + var size = new Vector2(ImGui.CalcTextSize(label).X + 16f * scale, ImGui.GetFrameHeight()); + var origin = ImGui.GetCursorScreenPos(); + + var clicked = ImGui.InvisibleButton(id, size); + var hovered = ImGui.IsItemHovered(); + var amount = StyleEngine.HoverState.Query(ImGui.GetID(id), hovered); + + if (amount > 0f) + dl.AddRectFilled( + origin, + origin + size, + ColourUtil.ApplyAlpha(ColourUtil.RgbaToAbgr(c.SurfaceHover), amount), + 3f * scale + ); + + var tint = ColourUtil.EnsureContrast( + ColourUtil.RgbaToAbgr(c.TextMuted), + ColourUtil.RgbaToAbgr(c.WindowBg), + 4.5f + ); + if (amount > 0f) + tint = ColourUtil.Lerp(tint, ColourUtil.RgbaToAbgr(c.Accent), amount * 0.6f); + + var textSize = ImGui.CalcTextSize(label); + dl.AddText(origin + (size - textSize) * 0.5f, tint, label); + + return clicked; + } + private void DrawFooter(bool showBack, bool showSkip, string primaryLabel, Action onPrimary) { var spacing = ImGui.GetStyle().ItemSpacing.Y; @@ -133,14 +278,14 @@ public sealed class FirstRunWizard : Window if (showBack) { - if (ImGui.Button(HellionStrings.Wizard_Nav_Back)) + if (DrawGhostLink("##wiz-back", HellionStrings.Wizard_Nav_Back)) _state.CurrentStep = Math.Max(1, _state.CurrentStep - 1); ImGui.SameLine(); } if (showSkip) { - if (ImGui.Button(HellionStrings.Wizard_Step1_Skip_Label)) + if (DrawGhostLink("##wiz-skip", HellionStrings.Wizard_Step1_Skip_Label)) { // Skip path = matches today's Cancel path: mark first-run // complete, save, close. No CommitPending — the user said @@ -158,13 +303,8 @@ public sealed class FirstRunWizard : Window if (rightX > ImGui.GetCursorPosX()) ImGui.SameLine(rightX); - using (ImRaii.PushColor(ImGuiCol.Button, ForgeBronze)) - using (ImRaii.PushColor(ImGuiCol.ButtonHovered, ForgeBronze)) - using (ImRaii.PushColor(ImGuiCol.ButtonActive, ForgeBronze)) - { - if (ImGui.Button($"{primaryLabel}##wizard-primary")) - onPrimary(); - } + if (DrawPrimaryPill(primaryLabel)) + onPrimary(); } private void DrawStepWelcome() @@ -172,48 +312,19 @@ public sealed class FirstRunWizard : Window ImGui.TextUnformatted(HellionStrings.Wizard_Step1_Title); ImGui.Spacing(); - // Fox-banner image: the embedded Hellion Forge fox artwork. The card - // behind the image gives the dark fox enough contrast against the - // plugin's dark UI so the logo reads clearly at a glance. - var banner = FoxBannerTexture.Shared.GetWrapOrDefault(); - if (banner is not null) + // Same scrolling frame as the other steps: the plugin notice is not + // optional reading, so it may not be the thing that falls off the + // bottom edge at a larger font or display scale. + var footerReserve = ImGui.GetFrameHeightWithSpacing() + ImGui.GetStyle().ItemSpacing.Y * 2f; + var bodyHeight = MathF.Max(1f, ImGui.GetContentRegionAvail().Y - footerReserve); + + using (ImRaii.PushColor(ImGuiCol.ChildBg, 0u)) + using (var body = ImRaii.Child("##wizard-welcome", new Vector2(-1f, bodyHeight))) { - const uint CardColor = 0xFFE8E8E8; // off-white fill so the dark fox pops - var imgHeight = 170f * ImGuiHelpers.GlobalScale; - var imgWidth = imgHeight * banner.Size.X / banner.Size.Y; - var pad = 14f * ImGuiHelpers.GlobalScale; - var cardWidth = imgWidth + pad * 2f; - var cardHeight = imgHeight + pad * 2f; - var rounding = 8f * ImGuiHelpers.GlobalScale; - - // Centre the card in the content region. Clamp to zero so the card - // never shifts left of the window edge on very narrow windows. - var offsetX = Math.Max(0f, (ImGui.GetContentRegionAvail().X - cardWidth) * 0.5f); - var cardOrigin = ImGui.GetCursorScreenPos() + new Vector2(offsetX, 0f); - - // Draw the rounded card behind the image, then place the image on top. - ImGui - .GetWindowDrawList() - .AddRectFilled( - cardOrigin, - cardOrigin + new Vector2(cardWidth, cardHeight), - CardColor, - rounding - ); - ImGui.SetCursorScreenPos(cardOrigin + new Vector2(pad, pad)); - ImGui.Image(banner.Handle, new Vector2(imgWidth, imgHeight)); - - // Advance the layout cursor past the full card so the content below - // starts at the right position and does not overlap the card. - ImGui.SetCursorScreenPos(cardOrigin); - ImGui.Dummy(new Vector2(cardWidth, cardHeight)); + if (body.Success) + DrawWelcomeBody(); } - ImGui.Spacing(); - ImGui.TextWrapped(HellionStrings.Wizard_Step1_Subtitle); - ImGui.Spacing(); - ImGui.TextWrapped(HellionStrings.Wizard_Step1_Footer_Hint); - DrawFooter( showBack: false, showSkip: true, @@ -222,65 +333,248 @@ public sealed class FirstRunWizard : Window ); } + private void DrawWelcomeBody() + { + var scale = StyleEngine.Metrics.Scale; + var c = Plugin.ThemeRegistry.Active.Colors; + var dl = ImGui.GetWindowDrawList(); + var surface = ColourUtil.RgbaToAbgr(c.WindowBg); + + // Mark beside the text, not stacked over it. Centred above a paragraph + // it left a column of empty window on either side, and the page read as + // things that happened to share a step. This is the shape the profile + // cards and the boutique callout use: the mark on the left, what it is + // about on the right. + var diameter = MedallionDiameterRaw * scale; + var gap = 18f * scale; + var width = ImGui.GetContentRegionAvail().X; + var textWidth = MathF.Max(1f, width - diameter - gap); + + var subtitle = HellionStrings.Wizard_Step1_Subtitle; + var subtitleHeight = ImGui.CalcTextSize(subtitle, false, textWidth).Y; + + var rowTop = ImGui.GetCursorScreenPos(); + var rowHeight = MathF.Max(diameter, subtitleHeight); + + DrawFoxMedallion( + rowTop + new Vector2(0f, MetricsMath.CenterY(rowHeight, diameter)), + diameter + ); + + dl.AddText( + ImGui.GetFont(), + ImGui.GetFontSize(), + new Vector2( + rowTop.X + diameter + gap, + rowTop.Y + MetricsMath.CenterY(rowHeight, subtitleHeight) + ), + ColourUtil.EnsureContrast(ColourUtil.RgbaToAbgr(c.TextPrimary), surface, 4.5f), + subtitle, + textWidth + ); + + ImGui.Dummy(new Vector2(width, rowHeight)); + ImGui.Spacing(); + + DrawPluginNotice(width); + + ImGui.Spacing(); + + // Both in the muted tone: where the plugin came from and how long this + // takes are context, not instructions. + var faint = ColourUtil.EnsureContrast(ColourUtil.RgbaToAbgr(c.TextMuted), surface, 4.5f); + DrawFaintParagraph(HellionStrings.Wizard_Step1_Heritage, width, faint); + ImGui.Spacing(); + DrawFaintParagraph(HellionStrings.Wizard_Step1_Footer_Hint, width, faint); + } + + private static void DrawFaintParagraph(string text, float width, uint colourAbgr) + { + var origin = ImGui.GetCursorScreenPos(); + var height = ImGui.CalcTextSize(text, false, width).Y; + + ImGui + .GetWindowDrawList() + .AddText(ImGui.GetFont(), ImGui.GetFontSize(), origin, colourAbgr, text, width); + + ImGui.Dummy(new Vector2(width, height)); + } + + // Third-party plugins sit outside what Square Enix's terms cover, and the + // game's producer has asked publicly that people not advertise them. The + // wizard is the one moment every user passes through, so it is where the + // ask belongs -- on its own surface, because a line of muted text under a + // welcome message is a line nobody reads. + // + // The notice names Say, Yell and Shout, and each translation spells them the + // way Language..resx does -- baked in rather than formatted at runtime, + // because the list is punctuated differently per language. That makes it a + // second place to fix whenever a ChatType_Say/Yell/Shout value changes: + // Spanish drifted exactly that way, carrying an untranslated "Shout" into + // the notice on 2026-08-19. + private void DrawPluginNotice(float width) + { + var scale = StyleEngine.Metrics.Scale; + var c = Plugin.ThemeRegistry.Active.Colors; + var dl = ImGui.GetWindowDrawList(); + + var text = HellionStrings.Wizard_Step1_PluginNotice; + var padX = 12f * scale; + var padY = 10f * scale; + var barWidth = 2f * scale; + var inner = MathF.Max(1f, width - barWidth - padX * 2f); + + var origin = ImGui.GetCursorScreenPos(); + var height = MeasureHangingNotice(text, inner) + padY * 2f; + var max = origin + new Vector2(width, height); + + var surface = ColourUtil.RgbaToAbgr(c.Surface); + var warn = ColourUtil.EnsureContrast(ColourUtil.RgbaToAbgr(c.StatusWarning), surface, 4.5f); + + dl.AddRectFilled(origin, max, surface); + dl.AddRectFilled(origin, new Vector2(origin.X + barWidth, max.Y), warn); + + DrawHangingNotice( + new Vector2(origin.X + barWidth + padX, origin.Y + padY), + text, + inner, + warn + ); + + ImGui.Dummy(new Vector2(width, height)); + } + + private const float MedallionDiameterRaw = 128f; + + // Contrast the disc has to reach against black. Higher washes the accent + // out towards white, lower keeps more colour and starts swallowing the + // grey brushwork in the artwork. This is the dial for how tinted the + // medallion reads. + private const float MedallionInkContrast = 9f; + + // The fox is drawn in black ink and the plugin is dark, so the artwork needs + // something light behind it. It used to get a hardcoded off-white rectangle, + // which read as a sheet of paper taped onto the window. + // + // A disc instead, tinted from the theme accent rather than neutral grey: it + // is still bright enough to carry black linework, but it is the window's own + // colour doing it, and a ring closes it off as a mark instead of a patch. + // The artwork itself is untouched -- it is the brand. + private void DrawFoxMedallion(Vector2 origin, float diameter) + { + var banner = FoxBannerTexture.Shared.GetWrapOrDefault(); + if (banner is null) + return; + + var scale = StyleEngine.Metrics.Scale; + var c = Plugin.ThemeRegistry.Active.Colors; + var dl = ImGui.GetWindowDrawList(); + + var radius = diameter * 0.5f; + var centre = origin + new Vector2(radius, radius); + + var accent = ColourUtil.RgbaToAbgr(c.Accent); + + // Measured against the ink rather than set to a fixed lightness. The + // artwork is black brushwork, so the disc has to clear it -- but + // EnsureContrast stops at the first step that does, which leaves as much + // of the theme accent in the disc as the drawing can afford. A fixed + // near-white cleared it by miles and looked like paper on the window. + var disc = ColourUtil.EnsureContrast(accent, 0xFF000000u, MedallionInkContrast); + dl.AddCircleFilled(centre, radius, disc, 64); + dl.AddCircle(centre, radius, accent, 64, MathF.Max(1f, 1.5f * scale)); + + // Short of the ring, because the ears reach the edge of the source + // square and would otherwise touch it. + var art = diameter * 0.82f; + var artSize = new Vector2(art, art * banner.Size.Y / banner.Size.X); + var artMin = centre - artSize * 0.5f; + dl.AddImage(banner.Handle, artMin, artMin + artSize); + } + + // The grid order, top-left to bottom-right, with the glyph each profile + // carries. Static because the pairing never changes -- the labels do, so + // those are looked up per frame and a language switch reaches them. + // ToIconString allocates on every call and caches nothing, so the four + // glyphs are resolved once here rather than per card per frame. + private static readonly (PrivacyProfile Profile, string Glyph, string Id)[] ProfileOrder = + [ + (PrivacyProfile.PrivacyFirst, FontAwesomeIcon.Lock.ToIconString(), "##profile-card-first"), + (PrivacyProfile.Casual, FontAwesomeIcon.Comments.ToIconString(), "##profile-card-casual"), + (PrivacyProfile.Roleplay, FontAwesomeIcon.TheaterMasks.ToIconString(), "##profile-card-rp"), + ( + PrivacyProfile.FullHistory, + FontAwesomeIcon.BookOpen.ToIconString(), + "##profile-card-full" + ), + ]; + + private static readonly string WarningGlyph = + FontAwesomeIcon.ExclamationTriangle.ToIconString(); + + // One card wore the badge while a second said "(recommended)" in its own + // heading, in all 25 languages -- the four-step rewrite moved the flag and + // left the older text where it was. Casual is the entry point most players + // want, so the suffix came out of the heading instead. + private const PrivacyProfile RecommendedProfile = PrivacyProfile.Casual; + + private const float CardPadXRaw = 14f; + private const float CardPadYRaw = 12f; + private const float CardGapRaw = 9f; + private const float CardAccentBarRaw = 2f; + + private static (string Heading, string Description) TextFor(PrivacyProfile profile) => + profile switch + { + PrivacyProfile.PrivacyFirst => ( + HellionStrings.Wizard_Profile_PrivacyFirst_Heading, + HellionStrings.Wizard_Profile_PrivacyFirst_Description + ), + PrivacyProfile.Casual => ( + HellionStrings.Wizard_Profile_Casual_Heading, + HellionStrings.Wizard_Profile_Casual_Description + ), + PrivacyProfile.Roleplay => ( + HellionStrings.Wizard_Profile_Roleplay_Heading, + HellionStrings.Wizard_Profile_Roleplay_Description + ), + _ => ( + HellionStrings.Wizard_Profile_FullHistory_Heading, + HellionStrings.Wizard_Profile_FullHistory_Description + ), + }; + + // Written for this card, translated into 25 languages, and drawn nowhere + // since the four-step rewrite dropped it on 2026-05-18. Full history turns + // the privacy filter off and keeps third-party messages indefinitely, which + // is exactly the case the notice is about -- so it belongs on the card where + // that choice is made, not in a resource file with no caller. + private static string? WarningFor(PrivacyProfile profile) => + profile == PrivacyProfile.FullHistory + ? HellionStrings.Wizard_Profile_FullHistory_GdprWarning + : null; + private void DrawStepPrivacy() { ImGui.TextUnformatted(HellionStrings.Wizard_Step2_Title); ImGui.Spacing(); - // Reserve footer height (separator + spacing + button row) so the - // 2x2 grid uses the rest of the window. - var footerReserve = - ImGui.GetFrameHeightWithSpacing() - + ImGui.GetStyle().ItemSpacing.Y * 3 - + ImGui.GetTextLineHeight(); - var grid = ImGui.GetContentRegionAvail(); - var cardWidth = (grid.X - ImGui.GetStyle().ItemSpacing.X) / 2f; - var cardHeight = (grid.Y - footerReserve - ImGui.GetStyle().ItemSpacing.Y) / 2f; + // DrawFooter pushes itself to the bottom of whatever room is left, so + // the grid has to stop short of it or the two fight over the same space. + var footerReserve = ImGui.GetFrameHeightWithSpacing() + ImGui.GetStyle().ItemSpacing.Y * 2f; + var gridHeight = MathF.Max(1f, ImGui.GetContentRegionAvail().Y - footerReserve); - // Top row. - DrawProfileCard( - PrivacyProfile.PrivacyFirst, - "🔒", - HellionStrings.Wizard_Profile_PrivacyFirst_Heading, - HellionStrings.Wizard_Profile_PrivacyFirst_Description, - recommended: false, - cardWidth, - cardHeight - ); - ImGui.SameLine(); - DrawProfileCard( - PrivacyProfile.Casual, - "💬", - HellionStrings.Wizard_Profile_Casual_Heading, - HellionStrings.Wizard_Profile_Casual_Description, - recommended: true, - cardWidth, - cardHeight - ); - - // Bottom row. - DrawProfileCard( - PrivacyProfile.Roleplay, - "🎭", - HellionStrings.Wizard_Profile_Roleplay_Heading, - HellionStrings.Wizard_Profile_Roleplay_Description, - recommended: false, - cardWidth, - cardHeight - ); - ImGui.SameLine(); - DrawProfileCard( - PrivacyProfile.FullHistory, - "📚", - HellionStrings.Wizard_Profile_FullHistory_Heading, - HellionStrings.Wizard_Profile_FullHistory_Description, - recommended: false, - cardWidth, - cardHeight - ); - - ImGui.Spacing(); - ImGui.TextDisabled(HellionStrings.Wizard_Step2_RecommendedFooter); + // Transparent, because the cards own their surfaces and a child + // background would draw a second panel around the set. The scroll is + // the point: four descriptions at 25 languages' worth of length do not + // fit a fixed 2x2 grid, and cutting a privacy choice off mid-sentence + // is not a thing this plugin gets to do. + using (ImRaii.PushColor(ImGuiCol.ChildBg, 0u)) + using (var grid = ImRaii.Child("##wizard-profiles", new Vector2(-1f, gridHeight))) + { + if (grid.Success) + DrawProfileGrid(); + } DrawFooter( showBack: true, @@ -290,148 +584,296 @@ public sealed class FirstRunWizard : Window ); } + private void DrawProfileGrid() + { + var gap = ImGui.GetStyle().ItemSpacing.X; + var cardWidth = MathF.Max(1f, (ImGui.GetContentRegionAvail().X - gap) / 2f); + + for (var i = 0; i < ProfileOrder.Length; i += 2) + { + var hasRight = i + 1 < ProfileOrder.Length; + + // Both cards in a row take the taller one's height. Sized apart + // they read as two unrelated tiles instead of one choice, and which + // description is longest changes with the language. + var height = MathF.Max( + MeasureProfileCard(ProfileOrder[i].Profile, cardWidth), + hasRight ? MeasureProfileCard(ProfileOrder[i + 1].Profile, cardWidth) : 0f + ); + + DrawProfileCard(ProfileOrder[i], cardWidth, height); + if (!hasRight) + continue; + + ImGui.SameLine(); + DrawProfileCard(ProfileOrder[i + 1], cardWidth, height); + } + } + + private static float CardInnerWidth(float width) => + MathF.Max(1f, width - (CardAccentBarRaw + CardPadXRaw * 2f) * StyleEngine.Metrics.Scale); + + // Measured rather than assumed, for the same reason SettingRow measures its + // description: the text wraps, and a card sized to one line would clip the + // rest of a privacy explanation. + private float MeasureProfileCard(PrivacyProfile profile, float width) + { + var scale = StyleEngine.Metrics.Scale; + var (_, description) = TextFor(profile); + + var inner = CardInnerWidth(width); + var height = + CardPadYRaw * 2f * scale + + TitleRowHeight() + + CardGapRaw * 2f * scale + + ImGui.CalcTextSize(description, false, inner).Y; + + if (WarningFor(profile) is { } warning) + height += CardGapRaw * scale + MeasureHangingNotice(warning, inner); + + return height; + } + + // The badge is a pill with a 22px floor, so it is taller than a line of + // text. Centring it into one would hang it over both edges of the title + // row, so the row takes the taller of the two -- and every card uses the + // same height, or a row of two would not line up. + private float TitleRowHeight() => MathF.Max(ImGui.GetTextLineHeight(), BadgeSize().Y); + + private int _badgeSizeFrame = -1; + private Vector2 _badgeSize; + + private Vector2 BadgeSize() + { + if (_badgeSizeFrame == ImGui.GetFrameCount()) + return _badgeSize; + + _badgeSizeFrame = ImGui.GetFrameCount(); + _badgeSize = StyleEngine.Widgets.Pill.CalcSize( + HellionStrings.Wizard_Profile_Recommended_Badge, + withDot: false + ); + return _badgeSize; + } + + // The notice hangs off its own icon column, so it wraps narrower than the + // description above it. + private float WarningTextWidth(float innerWidth) => MathF.Max(1f, innerWidth - WarningIndent()); + + private float MeasureHangingNotice(string text, float width) => + ImGui.CalcTextSize(text, false, WarningTextWidth(width)).Y; + + // Symbol in its own column, text hanging beside it. Shared by the welcome + // step's plugin notice and the full-history card, so the two read as the + // same kind of statement rather than two unrelated warnings. + private void DrawHangingNotice(Vector2 origin, string text, float width, uint colourAbgr) + { + var dl = ImGui.GetWindowDrawList(); + + using (Plugin.FontManager.FontAwesome.Push()) + dl.AddText(origin, colourAbgr, WarningGlyph); + + dl.AddText( + ImGui.GetFont(), + ImGui.GetFontSize(), + origin + new Vector2(WarningIndent(), 0f), + colourAbgr, + text, + WarningTextWidth(width) + ); + } + + private int _warningIndentFrame = -1; + private float _warningIndent; + + // Cached per frame: a font push allocates a lock and queues a deferred + // dispose, and the card measurement runs twice per row before anything is + // drawn. The glyph is the same one every time. + private float WarningIndent() + { + if (_warningIndentFrame == ImGui.GetFrameCount()) + return _warningIndent; + + _warningIndentFrame = ImGui.GetFrameCount(); + using (Plugin.FontManager.FontAwesome.Push()) + _warningIndent = + ImGui.CalcTextSize(WarningGlyph).X + CardGapRaw * StyleEngine.Metrics.Scale; + return _warningIndent; + } + + // Row's language as a tile: a resting surface because a card is something + // you press, an accent bar down the left edge when it is the chosen one, + // and a held hover over both. No rounding -- Row does not round either, and + // a rounded corner would cut the accent bar off at an angle. private void DrawProfileCard( - PrivacyProfile profile, - string emoji, - string heading, - string description, - bool recommended, + (PrivacyProfile Profile, string Glyph, string Id) card, float width, float height ) { - var isSelected = _state.PendingProfile == profile; - // GetStyleColorVec4 returns a pointer to the live style entry in - // Dalamud.Bindings.ImGui, which would require unsafe. Use the U32 - // packed-colour overload of PushColor for the default branch so we - // can stay in safe code while still matching the current border. - var borderColor = isSelected - ? ImGui.GetColorU32(ForgeBronze) - : ImGui.GetColorU32(ImGuiCol.Border); + var (profile, glyph, id) = card; + var scale = StyleEngine.Metrics.Scale; + var c = Plugin.ThemeRegistry.Active.Colors; + var dl = ImGui.GetWindowDrawList(); + var (heading, description) = TextFor(profile); - using var _border = ImRaii.PushColor(ImGuiCol.Border, borderColor); - using var child = ImRaii.Child( - $"##profile-card-{profile}", - new Vector2(width, height), - true + var origin = ImGui.GetCursorScreenPos(); + var size = new Vector2(width, height); + var max = origin + size; + var isSelected = _state.PendingProfile == profile; + + if (ImGui.InvisibleButton(id, size)) + _state.PendingProfile = profile; + var amount = StyleEngine.HoverState.Query(ImGui.GetID(id), ImGui.IsItemHovered()); + + var surface = ColourUtil.RgbaToAbgr(c.Surface); + var accent = ColourUtil.EnsureContrast(ColourUtil.RgbaToAbgr(c.Accent), surface, 3f); + + dl.AddRectFilled(origin, max, surface); + if (isSelected) + dl.AddRectFilled(origin, max, ColourUtil.ApplyAlpha(accent, 0.10f)); + if (amount > 0f) + dl.AddRectFilled( + origin, + max, + ColourUtil.ApplyAlpha(ColourUtil.RgbaToAbgr(c.SurfaceHover), amount) + ); + + dl.AddRect( + origin, + max, + isSelected ? accent : ColourUtil.RgbaToAbgr(c.Border), + 0f, + ImDrawFlags.None, + MathF.Max(1f, scale) ); - if (!child.Success) + + if (isSelected) + dl.AddRectFilled( + origin, + new Vector2(origin.X + CardAccentBarRaw * scale, max.Y), + accent + ); + + var innerLeft = origin.X + (CardAccentBarRaw + CardPadXRaw) * scale; + var innerWidth = CardInnerWidth(width); + var titleHeight = TitleRowHeight(); + var y = origin.Y + CardPadYRaw * scale; + + var titleAbgr = ColourUtil.EnsureContrast( + ColourUtil.RgbaToAbgr(c.TextPrimary), + surface, + 4.5f + ); + + // The badge is measured before the title is drawn, so a long heading + // gets clipped short of it instead of running underneath. + var badgeWidth = 0f; + if (profile == RecommendedProfile) + { + var badgeSize = BadgeSize(); + badgeWidth = badgeSize.X + CardGapRaw * scale; + StyleEngine.Widgets.Pill.Draw( + new Vector2( + innerLeft + innerWidth - badgeSize.X, + y + MetricsMath.CenterY(titleHeight, badgeSize.Y) + ), + HellionStrings.Wizard_Profile_Recommended_Badge, + accent, + ColourUtil.EnsureContrast(ColourUtil.RgbaToAbgr(c.WindowBg), accent, 4.5f) + ); + } + + var x = innerLeft; + using (Plugin.FontManager.FontAwesome.Push()) + { + var glyphSize = ImGui.CalcTextSize(glyph); + dl.AddText( + new Vector2(x, y + MetricsMath.CenterY(titleHeight, glyphSize.Y)), + isSelected + ? accent + : ColourUtil.EnsureContrast(ColourUtil.RgbaToAbgr(c.TextMuted), surface, 3f), + glyph + ); + x += glyphSize.X + CardGapRaw * scale; + } + + // Tracked caps, the shape the finished windows use to give a name rank + // without a bold cut -- the plugin does not ship one. + var titleRight = innerLeft + innerWidth - badgeWidth; + if (titleRight > x) + { + var titleY = y + MetricsMath.CenterY(titleHeight, ImGui.GetTextLineHeight()); + dl.PushClipRect(new Vector2(x, y), new Vector2(titleRight, y + titleHeight), true); + dl.DrawTrackedText( + new Vector2(x, titleY), + heading.ToUpperInvariant(), + titleAbgr, + 1.4f * scale + ); + dl.PopClipRect(); + } + + y += titleHeight + CardGapRaw * scale; + + dl.DrawFadeRule( + new Vector2(innerLeft, y), + innerWidth, + ColourUtil.RgbaToAbgr(c.Border), + MathF.Max(1f, scale) + ); + + y += CardGapRaw * scale; + + dl.AddText( + ImGui.GetFont(), + ImGui.GetFontSize(), + new Vector2(innerLeft, y), + ColourUtil.EnsureContrast(ColourUtil.RgbaToAbgr(c.TextMuted), surface, 4.5f), + description, + innerWidth + ); + + if (WarningFor(profile) is not { } warning) return; - // InvisibleButton over the full card area, then SetCursorScreenPos - // back to draw the heading/description content on top. Selectable - // would be semantically wrong here — the card is a standalone - // choice tile, not a list-item inside a list/menu. The button - // takes the click for the entire card area, and IsItemHovered() - // on it (if we wire one up later) would naturally cover the full - // tile. Visual feedback comes from the border colour above. - var startPos = ImGui.GetCursorScreenPos(); - var cardArea = ImGui.GetContentRegionAvail(); - if (ImGui.InvisibleButton($"##profile-hit-{profile}", cardArea)) - _state.PendingProfile = profile; + y += ImGui.CalcTextSize(description, false, innerWidth).Y + CardGapRaw * scale; - ImGui.SetCursorScreenPos(startPos); - - ImGui.TextUnformatted($"{emoji} {heading}{(recommended ? " ★" : string.Empty)}"); - ImGui.Spacing(); - ImGui.Separator(); - ImGui.Spacing(); - ImGui.TextWrapped(description); + DrawHangingNotice( + new Vector2(innerLeft, y), + warning, + innerWidth, + ColourUtil.EnsureContrast(ColourUtil.RgbaToAbgr(c.StatusWarning), surface, 4.5f) + ); } private void DrawStepPowerSettings() { // Seed only the recommendation field here. Other fields remain null // until the user touches the corresponding control. - // Spec FR-4: the wizard explicitly recommends - // FilterIncludePreviousSessions = true (the Config default is false). - // The other four fields (AutoTellTabsHistoryPreload, UseCompactDensity, - // PrettierTimestamps, Theme) follow the generic null-semantics from - // Spec Z.176: a null pending means the user did not touch that control, - // so CommitPending must not write back. They are read live from - // Plugin.Config below for the ImGui ref-binding but never seeded into - // Pending* without a user gesture. + // The wizard actively recommends FilterIncludePreviousSessions = true, + // where the config default is false, so this one is seeded. + // + // The rest are not: a null pending means the user never touched that + // control, and CommitPending must not write those back. They are read + // live from Plugin.Config for display, never seeded without a gesture. _state.PendingFilterIncludePreviousSessions ??= true; ImGui.TextUnformatted(HellionStrings.Wizard_Step3_Title); ImGui.Spacing(); - // History section. - using (ImRaii.PushColor(ImGuiCol.Text, ForgeBronze)) - ImGui.TextUnformatted(HellionStrings.Wizard_Step3_Section_History); + // Same scrolling frame as the profile grid, for the same reason: a + // SettingRow carries its own padding, so four of them plus three + // headings are taller than the eight stock widgets they replaced, and + // display scaling makes that worse rather than better. + var footerReserve = ImGui.GetFrameHeightWithSpacing() + ImGui.GetStyle().ItemSpacing.Y * 2f; + var bodyHeight = MathF.Max(1f, ImGui.GetContentRegionAvail().Y - footerReserve); - // One checkbox, not two. LoadPreviousSession was asked for here, shown - // as applied in the summary and written to the config, and no code in - // the plugin has ever read it -- so the wizard was collecting a - // decision and reporting an effect that never happened. Its partner - // does the work on its own. - var filterPrev = _state.PendingFilterIncludePreviousSessions ?? true; - if ( - ImGui.Checkbox( - HellionStrings.Wizard_Step3_FilterIncludePreviousSessions_Label, - ref filterPrev - ) - ) + using (ImRaii.PushColor(ImGuiCol.ChildBg, 0u)) + using (var body = ImRaii.Child("##wizard-power", new Vector2(-1f, bodyHeight))) { - _state.PendingFilterIncludePreviousSessions = filterPrev; - } - - ImGui.Spacing(); - - // Tell-Tabs section. - using (ImRaii.PushColor(ImGuiCol.Text, ForgeBronze)) - ImGui.TextUnformatted(HellionStrings.Wizard_Step3_Section_TellTabs); - - var preload = - _state.PendingAutoTellTabsHistoryPreload ?? Plugin.Config.AutoTellTabsHistoryPreload; - if ( - ImGui.SliderInt( - HellionStrings.Wizard_Step3_AutoTellTabsHistoryPreload_Label, - ref preload, - 0, - 100 - ) - ) - _state.PendingAutoTellTabsHistoryPreload = preload; - - ImGui.Spacing(); - - // Visual section. - using (ImRaii.PushColor(ImGuiCol.Text, ForgeBronze)) - ImGui.TextUnformatted(HellionStrings.Wizard_Step3_Section_Visual); - - var compact = _state.PendingUseCompactDensity ?? Plugin.Config.UseCompactDensity; - if (ImGui.Checkbox(HellionStrings.Wizard_Step3_UseCompactDensity_Label, ref compact)) - _state.PendingUseCompactDensity = compact; - - // Theme dropdown — built-ins only. Custom themes are power-user - // territory and would clutter the first-run flow. - var currentSlug = _state.PendingTheme ?? Plugin.Config.Theme; - var builtIns = Plugin.ThemeRegistry.AllBuiltIns().ToList(); - var currentIndex = builtIns.FindIndex(t => - string.Equals(t.Slug, currentSlug, StringComparison.OrdinalIgnoreCase) - ); - if (currentIndex < 0) - currentIndex = 0; - - using ( - var combo = ImRaii.Combo( - HellionStrings.Wizard_Step3_Theme_Label, - builtIns[currentIndex].Name - ) - ) - { - if (combo.Success) - { - for (var i = 0; i < builtIns.Count; i++) - { - var isSelected = i == currentIndex; - if (ImGui.Selectable(builtIns[i].Name, isSelected)) - _state.PendingTheme = builtIns[i].Slug; - if (isSelected) - ImGui.SetItemDefaultFocus(); - } - } + if (body.Success) + DrawPowerSettingsBody(); } DrawFooter( @@ -442,6 +884,191 @@ public sealed class FirstRunWizard : Window ); } + // The transient widget overloads throughout, never the Func/Action pair: + // those save the config on every click, and this step is staged until the + // user reaches Finish. A wizard that wrote as it went would leave half a + // profile behind on "decide later". + private void DrawPowerSettingsBody() + { + DrawHeading(HellionStrings.Wizard_Step3_Section_History); + + // One checkbox, not two. LoadPreviousSession was asked for here, shown + // as applied in the summary and written to the config, and no code in + // the plugin has ever read it -- so the wizard was collecting a + // decision and reporting an effect that never happened. Its partner + // does the work on its own. + _state.PendingFilterIncludePreviousSessions = _widgets.ToggleRow( + ImGui.GetID("wizard.history.previous"u8), + HellionStrings.Wizard_Step3_FilterIncludePreviousSessions_Label, + null, + _state.PendingFilterIncludePreviousSessions ?? true + ); + + ImGui.Spacing(); + + DrawHeading(HellionStrings.Wizard_Step3_Section_TellTabs); + + var preload = + _state.PendingAutoTellTabsHistoryPreload ?? Plugin.Config.AutoTellTabsHistoryPreload; + var newPreload = _widgets.SliderIntRow( + ImGui.GetID("wizard.telltabs.preload"u8), + HellionStrings.Wizard_Step3_AutoTellTabsHistoryPreload_Label, + null, + preload, + 0, + 100 + ); + if (newPreload != preload) + _state.PendingAutoTellTabsHistoryPreload = newPreload; + + ImGui.Spacing(); + + DrawHeading(HellionStrings.Wizard_Step3_Section_Visual); + + var compact = _state.PendingUseCompactDensity ?? Plugin.Config.UseCompactDensity; + var newCompact = _widgets.ToggleRow( + ImGui.GetID("wizard.visual.compact"u8), + HellionStrings.Wizard_Step3_UseCompactDensity_Label, + null, + compact + ); + if (newCompact != compact) + _state.PendingUseCompactDensity = newCompact; + + DrawThemeRow(); + } + + // Built-ins only. Custom themes are power-user territory and would clutter + // the first-run flow. + private void DrawThemeRow() + { + var currentSlug = _state.PendingTheme ?? Plugin.Config.Theme; + + _widgets.Row( + ImGui.GetID("wizard.visual.theme"u8), + HellionStrings.Wizard_Step3_Theme_Label, + null, + ctx => + { + if ( + DrawDropdownAnchor( + "##wizard-theme", + Plugin.ThemeRegistry.Get(currentSlug).Name, + ctx.ControlOrigin, + ctx.ControlWidth + ) + ) + ImGui.OpenPopup("##wizard-theme-picker"); + + DrawThemePickerPopup(currentSlug); + } + ); + } + + private void DrawThemePickerPopup(string currentSlug) + { + if (!ImGui.BeginPopup("##wizard-theme-picker")) + return; + + try + { + // The popup must be sized from its rows: ImGui does not grow one for + // draw-list content, so without this the longest theme name clips. + var width = 0f; + foreach (var theme in Plugin.ThemeRegistry.AllBuiltIns()) + width = MathF.Max( + width, + StyleEngine.Widgets.PopupRow.CalcWidth(theme.Name, null, Plugin.FontManager) + ); + + ImGui.Dummy(new Vector2(width, 0f)); + + var i = 0; + foreach (var theme in Plugin.ThemeRegistry.AllBuiltIns()) + { + var isCurrent = string.Equals( + theme.Slug, + currentSlug, + StringComparison.OrdinalIgnoreCase + ); + if ( + StyleEngine.Widgets.PopupRow.Draw( + $"##wizard-theme-{i++}", + theme.Name, + isCurrent, + Plugin.FontManager + ) + ) + { + _state.PendingTheme = theme.Slug; + ImGui.CloseCurrentPopup(); + } + } + } + finally + { + ImGui.EndPopup(); + } + } + + // The field shape from Boutique.Inputs: draw the surface, then put the + // control on top of it -- rather than pushing FrameBg colours at an + // ImGui.Combo and leaving it the arrow button ImGui draws by default. + private bool DrawDropdownAnchor(string id, string value, Vector2 origin, float width) + { + var scale = StyleEngine.Metrics.Scale; + var c = Plugin.ThemeRegistry.Active.Colors; + var dl = ImGui.GetWindowDrawList(); + var size = new Vector2(width, ImGui.GetFrameHeight()); + + ImGui.SetCursorScreenPos(origin); + var clicked = ImGui.InvisibleButton(id, size); + var amount = StyleEngine.HoverState.Query(ImGui.GetID(id), ImGui.IsItemHovered()); + + var max = origin + size; + var surface = ColourUtil.RgbaToAbgr(c.FrameBg); + var accent = ColourUtil.EnsureContrast(ColourUtil.RgbaToAbgr(c.Accent), surface, 3f); + var rounding = 3f * scale; + + dl.AddRectFilled(origin, max, surface, rounding); + dl.AddRect( + origin, + max, + ColourUtil.Lerp(ColourUtil.RgbaToAbgr(c.Border), accent, amount), + rounding, + ImDrawFlags.None, + MathF.Max(1f, scale) + ); + + // A filled triangle, the shape Character Select+ settled on for its own + // pickers. A glyph would mean pushing the icon font for three pixels of + // arrow. + var padX = 10f * scale; + var r = 4f * scale; + var centre = new Vector2(max.X - padX - r, origin.Y + size.Y * 0.5f); + var muted = ColourUtil.EnsureContrast(ColourUtil.RgbaToAbgr(c.TextMuted), surface, 3f); + dl.AddTriangleFilled( + centre + new Vector2(-r, -r * 0.5f), + centre + new Vector2(r, -r * 0.5f), + centre + new Vector2(0f, r * 0.7f), + ColourUtil.Lerp(muted, accent, amount) + ); + + // Clipped short of the chevron so a long theme name cannot run under it. + dl.PushClipRect(origin, new Vector2(centre.X - r, max.Y), true); + dl.AddText( + new Vector2( + origin.X + padX, + origin.Y + MetricsMath.CenterY(size.Y, ImGui.GetTextLineHeight()) + ), + ColourUtil.EnsureContrast(ColourUtil.RgbaToAbgr(c.TextPrimary), surface, 4.5f), + value + ); + dl.PopClipRect(); + + return clicked; + } + private void DrawStepDone() { ImGui.TextUnformatted(HellionStrings.Wizard_Step4_Title); @@ -453,7 +1080,7 @@ public sealed class FirstRunWizard : Window var avail = ImGui.GetContentRegionAvail(); ImGui.Dummy(new Vector2((avail.X - checkSize.X) * 0.5f, 0)); ImGui.SameLine(); - using (ImRaii.PushColor(ImGuiCol.Text, ForgeBronze)) + using (ImRaii.PushColor(ImGuiCol.Text, AccentVec4())) ImGui.TextUnformatted(checkmark); ImGui.Spacing(); @@ -522,10 +1149,10 @@ public sealed class FirstRunWizard : Window ImGui.Spacing(); - // Inline FR-3 hint with placeholder for preload count. + // Inline hint, with the preload count filled in. var preloadForHint = _state.PendingAutoTellTabsHistoryPreload ?? Plugin.Config.AutoTellTabsHistoryPreload; - using (ImRaii.PushColor(ImGuiCol.Text, ForgeBronze)) + using (ImRaii.PushColor(ImGuiCol.Text, AccentVec4())) ImGui.TextWrapped(string.Format(HellionStrings.Wizard_Step4_TestHint, preloadForHint)); ImGui.Spacing(); @@ -670,5 +1297,17 @@ public sealed class FirstRunWizard : Window public int? PendingAutoTellTabsHistoryPreload { get; set; } public bool? PendingUseCompactDensity { get; set; } public string? PendingTheme { get; set; } + + // Back to null, not to the config's current values: null is what tells + // CommitPending the user never touched that control. + public void Reset() + { + CurrentStep = 1; + PendingProfile = null; + PendingFilterIncludePreviousSessions = null; + PendingAutoTellTabsHistoryPreload = null; + PendingUseCompactDensity = null; + PendingTheme = null; + } } } diff --git a/HellionChat/Ui/InputPreview.cs b/HellionChat/Ui/InputPreview.cs index 28af46e..8de71c9 100644 --- a/HellionChat/Ui/InputPreview.cs +++ b/HellionChat/Ui/InputPreview.cs @@ -54,7 +54,7 @@ internal sealed class InputPreview : Window DisableWindowSounds = true; IsOpen = true; - // TODO Polish-Sweep: remove discard once logging call-sites exist + // TODO: remove discard once logging call sites exist _ = _logger; } @@ -168,7 +168,7 @@ internal sealed class InputPreview : Window { ImGui.TextUnformatted(Language.Options_Preview_Header); - // Primary path (A2) resets the Lender counter in MainWindow.Draw(); + // Primary path resets the Lender counter in MainWindow.Draw(); // this fallback covers the edge-case where MainWindow is closed but // InputPreview is still open, preventing handler pool growth. if (!_mainWindow.IsOpen) diff --git a/HellionChat/Ui/StyleEngine/DrawListExtensions.cs b/HellionChat/Ui/StyleEngine/DrawListExtensions.cs index 62579c4..caa3985 100644 --- a/HellionChat/Ui/StyleEngine/DrawListExtensions.cs +++ b/HellionChat/Ui/StyleEngine/DrawListExtensions.cs @@ -12,7 +12,7 @@ namespace HellionChat.Ui.StyleEngine; // ABGR before delegating to ImDrawList. internal static class DrawListExtensions { - // A1 accent-tint (Variante A): how far the white sweep is pulled toward + // Accent tint: how far the white sweep is pulled toward // the element's accent hue. Kept low so the sheen reads as a tinted // highlight, not a saturated accent flash (effect level "subtle"). private const float SheenTintStrength = 0.35f; diff --git a/HellionChat/Ui/StyleEngine/Metrics.cs b/HellionChat/Ui/StyleEngine/Metrics.cs index a836dff..e9afd83 100644 --- a/HellionChat/Ui/StyleEngine/Metrics.cs +++ b/HellionChat/Ui/StyleEngine/Metrics.cs @@ -9,7 +9,7 @@ namespace HellionChat.Ui.StyleEngine; // compare an unscaled value (Sidebar.GetWidth, the width slider bounds). // // Deliberately NOT part of ThemeLayout: that record is serialised into theme -// JSON, and layout customisation is out of scope per the master spec. +// JSON, and layout customisation is deliberately out of scope. internal static class Metrics { // --- Sidebar --- @@ -65,7 +65,7 @@ internal static class Metrics return _cachedScale; // Safe variant: GlobalScale throws while the interface manager is - // still coming up, and Block F pulls Metrics into more call sites. + // still coming up, and Metrics reaches more call sites than it did. _cachedScale = ImGuiHelpers.GlobalScaleSafe; _cachedFrame = frame; return _cachedScale; diff --git a/HellionChat/Ui/StyleEngine/TypeScale.cs b/HellionChat/Ui/StyleEngine/TypeScale.cs index 5316b12..215e1fc 100644 --- a/HellionChat/Ui/StyleEngine/TypeScale.cs +++ b/HellionChat/Ui/StyleEngine/TypeScale.cs @@ -23,7 +23,7 @@ internal enum TypeRole // exactly Body and never be pushed -- a value with no call site, which is the one // thing this whole style track exists to stop. // -// The factors are defaults, not constants. The master spec puts typography under +// The factors are defaults, not constants. Typography is meant to sit under // theme control rather than user control, and ThemeTypography already exists as // the extension point for exactly that. A const would wall it off. What is // deliberately absent either way is a user-facing slider per role. diff --git a/HellionChat/Ui/StyleEngine/Widgets/ChannelHeader.cs b/HellionChat/Ui/StyleEngine/Widgets/ChannelHeader.cs index c738784..9620256 100644 --- a/HellionChat/Ui/StyleEngine/Widgets/ChannelHeader.cs +++ b/HellionChat/Ui/StyleEngine/Widgets/ChannelHeader.cs @@ -18,8 +18,8 @@ namespace HellionChat.Ui.StyleEngine.Widgets; // // So: no plate, but a tint. A tenth-opacity accent wash falling from the top // edge -- colour as atmosphere rather than as a box, which is what the plate got -// wrong. Flo picked it from the lab over the rule-only variant; at this strength -// it survives the violet themes that killed the filled bar. +// wrong. Picked from the lab over the rule-only variant; at this strength it +// survives the violet themes that killed the filled bar. // // Which face draws what is not a style choice here, it is a constraint. The meta // face has a glyph range of ASCII plus a middle dot, so only the world name and diff --git a/HellionChat/Ui/Windows/ChannelPopoutPool.cs b/HellionChat/Ui/Windows/ChannelPopoutPool.cs index 4288348..052a474 100644 --- a/HellionChat/Ui/Windows/ChannelPopoutPool.cs +++ b/HellionChat/Ui/Windows/ChannelPopoutPool.cs @@ -7,7 +7,7 @@ namespace HellionChat.Ui.Windows; // windows via the injected factory, all registered once in the WindowSystem // (PluginLifecycle.RegisterWindows, framework thread). Open/Close is IsOpen + // Bind/Unbind only — NEVER runtime AddWindow/RemoveWindow (v1.4.9 Stage-2 -// freeze lesson). Pure DI-sink: no PayloadHandler in the ctor (plan §B.2). +// freeze lesson). Pure DI-sink: no PayloadHandler in the ctor. internal sealed class ChannelPopoutPool { private readonly List _instances; @@ -30,7 +30,7 @@ internal sealed class ChannelPopoutPool // Route each window's in-body close through the pool so closing releases // the slot. Wired here (post-construction) rather than via ctor to avoid - // a Window->Pool edge that would re-enter pool resolution (plan §B.2). + // a Window->Pool edge that would re-enter pool resolution. foreach (var window in _instances) window.CloseRequested = TryClose; } @@ -44,7 +44,7 @@ internal sealed class ChannelPopoutPool // A popped tab gets its own input bar, so strip stale tell state first — // otherwise a popped-out stale-tell tab would be a send surface that // bypasses the click-path activation strip. Previous = the main window's - // active tab; popping the active tab itself must not strip (TR-4 guard). + // active tab; popping the active tab itself must not strip (guard). TabLifecycleHelpers.OnTabActivated(tab, Plugin.Instance.MainWindow?.ActiveTab); var slot = _slots.TryReserve(tab.Identifier); diff --git a/HellionChat/Ui/Windows/ChannelPopoutWindow.cs b/HellionChat/Ui/Windows/ChannelPopoutWindow.cs index 834f7cd..9075f4a 100644 --- a/HellionChat/Ui/Windows/ChannelPopoutWindow.cs +++ b/HellionChat/Ui/Windows/ChannelPopoutWindow.cs @@ -13,7 +13,7 @@ namespace HellionChat.Ui.Windows; // One pre-allocated pop-out window bound to a single Tab. Pure DI-sink: the // PayloadHandler arrives via AttachPayloadHandler (post-build setter), NEVER -// via ctor — see plan §B.2. The ###id carries the slot index so all N +// via ctor. The ###id carries the slot index so all N // instances are unique for WindowSystem.AddWindow and ImGui state is stable // per slot (not per bound tab). internal sealed class ChannelPopoutWindow : Window, IFocusableChatWindow @@ -66,10 +66,10 @@ internal sealed class ChannelPopoutWindow : Window, IFocusableChatWindow // Wired post-build by ChannelPopoutPool so closing routes through the pool // (which owns the slot map). The window can't reach the pool by ctor without - // a DI cycle, so the pool sets this after construction. See plan §B.2. + // a DI cycle, so the pool sets this after construction. public Action? CloseRequested { get; set; } - // Post-build setter — see plan §B.2. Wired by ChannelPopoutInitHostedService. + // Post-build setter. Wired by ChannelPopoutInitHostedService. public void AttachPayloadHandler(PayloadHandler handler) => _messages.AttachPayloadHandler(handler); @@ -78,7 +78,7 @@ internal sealed class ChannelPopoutWindow : Window, IFocusableChatWindow Bound = tab; var isTell = tab is { IsTempTab: true, TellTarget: { } target } && target.IsSet(); - // Master §4.3 default sizes: Tell is the more compact conversation window. + // Default sizes: Tell is the more compact conversation window. Size = isTell ? new Vector2(380f, 320f) : new Vector2(420f, 320f); SizeCondition = ImGuiCond.FirstUseEver; @@ -103,7 +103,7 @@ internal sealed class ChannelPopoutWindow : Window, IFocusableChatWindow } // IFocusableChatWindow — this pop-out's own InputBar carries the focus state - // the keybind tail checks when deciding whether to route at this surface (C3). + // the keybind tail checks when deciding whether to route at this surface. public bool HasFocusedInput => _input.IsFocused; // Arm-and-hold the one-frame Activate flag; the pop-out's Draw applies the diff --git a/HellionChat/Ui/Windows/InputBarLabWindow.cs b/HellionChat/Ui/Windows/InputBarLabWindow.cs index 17f8f52..edf2b4d 100644 --- a/HellionChat/Ui/Windows/InputBarLabWindow.cs +++ b/HellionChat/Ui/Windows/InputBarLabWindow.cs @@ -13,8 +13,8 @@ namespace HellionChat.Ui.Windows; // Variants of UI elements, drawn side by side so decisions are made by looking // rather than by imagining. Reachable with /hellion lab. // -// Permanent, by Flo's call: a dev playground for seeing ideas in-game against -// the live theme. The radios default to whatever shipped, so the window also +// Permanent: a playground for seeing ideas in-game against the live theme. +// The radios default to whatever shipped, so the window also // documents which variant won and what it beat. // // It exists because the alternative was drawing mockups, and mockups are what diff --git a/HellionChat/Ui/Windows/MainWindow.cs b/HellionChat/Ui/Windows/MainWindow.cs index bd07ea3..fa6996e 100644 --- a/HellionChat/Ui/Windows/MainWindow.cs +++ b/HellionChat/Ui/Windows/MainWindow.cs @@ -82,14 +82,14 @@ internal sealed class MainWindow : Window, IFocusableChatWindow RespectCloseHotkey = false; } - // UI-12: per-window focus-dependent opacity. ResolveBgAlpha stays guard-free + // Per-window focus-dependent opacity. ResolveBgAlpha stays guard-free // and pure so the self-test can drive it directly; PreDraw owns the guard + // wiring. 1.5.6 parity (focused → WindowOpacity, unfocused → // WindowOpacityInactive, ChatLogWindow.PreOpenCheck 1d3b429:724). internal float ResolveBgAlpha(bool isFocused) => isFocused ? Plugin.Config.WindowOpacity : Plugin.Config.WindowOpacityInactive; - // B1-2 / P7: rebuild flags from a fresh base every frame so toggling + // Rebuild flags from a fresh base every frame so toggling // CanMove/CanResize/ShowTitleBar back on actually CLEARS NoMove/NoResize/ // NoTitleBar (not accumulating). Move/resize/title-bar logic as 1.5.6 // (ChatLogWindow.PreOpenCheck 1d3b429:703-710); base flags = today's @@ -240,7 +240,7 @@ internal sealed class MainWindow : Window, IFocusableChatWindow } // IFocusableChatWindow — the keybind tail resolves which surface owns the - // input focus before routing a channel-set/REPLY/prefill at it (C3). + // input focus before routing a channel-set/REPLY/prefill at it. public bool HasFocusedInput => _input.IsFocused; // Arm-and-hold: field writes only, safe from the framework thread; the draw diff --git a/HellionChat/Ui/Windows/SettingsWindow.cs b/HellionChat/Ui/Windows/SettingsWindow.cs index 23b6455..aba18ee 100644 --- a/HellionChat/Ui/Windows/SettingsWindow.cs +++ b/HellionChat/Ui/Windows/SettingsWindow.cs @@ -9,7 +9,7 @@ using Microsoft.Extensions.Logging; namespace HellionChat.Ui.Windows; -// `internal` to match the Plugin.SettingsWindow property in W2; `public` here +// `internal` to match the Plugin.SettingsWindow property; `public` here // would raise CS0053 against the internal members. Matches MainWindow shape. internal sealed class SettingsWindow : Window { diff --git a/HellionChat/Util/AutoTranslate.cs b/HellionChat/Util/AutoTranslate.cs index f92ccd3..5d3b7ce 100644 --- a/HellionChat/Util/AutoTranslate.cs +++ b/HellionChat/Util/AutoTranslate.cs @@ -62,9 +62,9 @@ internal static class AutoTranslate { var sw = Stopwatch.StartNew(); AllEntries(); - // v1.4.9 R3 profiling: Information so the xllog tail surfaces this + // v1.4.9 profiling: Information so the xllog tail surfaces this // without a Debug filter. Belt-and-suspenders for future plugin-load - // regressions; remains in place after Sub-Task 3.4 Befund. + // regressions. Plugin.LogProxy.Information( $"Warming up auto-translate took {sw.ElapsedMilliseconds}ms" ); diff --git a/HellionChat/Util/ChunkUtil.cs b/HellionChat/Util/ChunkUtil.cs index d0b36f1..ea684ca 100755 --- a/HellionChat/Util/ChunkUtil.cs +++ b/HellionChat/Util/ChunkUtil.cs @@ -9,244 +9,6 @@ namespace HellionChat.Util; internal static class ChunkUtil { - // internal static IEnumerable ToChunks(ReadOnlySeString msg, ChunkSource source, ChatType? defaultColour) - // { - // var chunks = new List(); - // - // var italic = false; - // var foreground = new Stack(); - // var glow = new Stack(); - // Payload? link = null; - // - // void Append(string text) - // { - // chunks.Add(new TextChunk(source, link, text) - // { - // FallbackColour = defaultColour, - // Foreground = foreground.Count > 0 ? foreground.Peek() : null, - // Glow = glow.Count > 0 ? glow.Peek() : null, - // Italic = italic, - // }); - // } - // - // foreach (var payload in msg) - // { - // if (payload.Type == ReadOnlySePayloadType.Text) - // { - // // We don't want to parse any null string - // var str = payload.ToString(); - // var nulIndex = str.IndexOf('\0'); - // if (nulIndex > 0) - // str = str[..nulIndex]; - // if (string.IsNullOrEmpty(str)) - // continue; - // - // Append(str); - // continue; - // } - // - // switch (payload.MacroCode) - // { - // case MacroCode.Italic: - // var newStatus = payload.TryGetExpression(out var expression) && expression.TryGetUInt(out var value) && value == 1; - // italic = newStatus; - // break; - // case MacroCode.Color: - // if (payload.TryGetExpression(out var eColor)) - // { - // if (eColor.TryGetPlaceholderExpression(out var ph) && ph == (int)ExpressionType.StackColor) - // { - // if (foreground.Count > 0) - // foreground.Pop(); - // } - // else if (TryResolveUInt(eColor, out var eColorVal)) - // { - // var color = ColourUtil.ArgbToRgba(eColorVal); - // - // if (color > 0) - // foreground.Push(color); - // else if (foreground.Count > 0) // Push the previous color as we don't want invisible text - // foreground.Push(foreground.Peek()); - // } - // } - // break; - // case MacroCode.EdgeColor: - // if (payload.TryGetExpression(out eColor)) - // { - // if (eColor.TryGetPlaceholderExpression(out var ph) && ph == (int)ExpressionType.StackColor) - // { - // if (glow.Count > 0) - // glow.Pop(); - // } - // else if (TryResolveUInt(eColor, out var eColorVal)) - // { - // glow.Push(ColourUtil.ArgbToRgba(eColorVal)); - // } - // } - // break; - // case MacroCode.ColorType: - // if (!payload.TryGetExpression(out var eColorType) || !eColorType.TryGetUInt(out var eColorTypeVal)) - // { - // if (foreground.Count > 0) - // foreground.Pop(); - // break; - // } - // - // if (eColorTypeVal == 0) - // { - // if (foreground.Count > 0) - // foreground.Pop(); - // } - // else if (Sheets.UIColorSheet.TryGetRow(eColorTypeVal, out var row)) - // { - // foreground.Push(row.Dark); - // } - // break; - // case MacroCode.EdgeColorType: - // if (!payload.TryGetExpression(out var eEdgeColor) || !eEdgeColor.TryGetUInt(out var eEdgeColorVal)) - // { - // if (glow.Count > 0) - // glow.Pop(); - // break; - // } - // - // if (eEdgeColorVal == 0) - // { - // if (glow.Count > 0) - // glow.Pop(); - // } - // else if (Sheets.UIColorSheet.TryGetRow(eEdgeColorVal, out var row)) - // { - // glow.Push(row.Dark); - // } - // break; - // case MacroCode.Fixed: - // if (!payload.TryGetExpression(out var expr1, out var expr2)) - // break; - // - // if (expr1.TryGetUInt(out var group) && expr2.TryGetUInt(out var key)) - // { - // chunks.Add(new IconChunk(source, null, BitmapFontIcon.AutoTranslateBegin)); - // using var rssb = new RentedSeStringBuilder(); - // var translatePayload = rssb.Builder - // .BeginMacro(MacroCode.Fixed) - // .AppendUIntExpression(group - 1) - // .AppendUIntExpression(key) - // .EndMacro() - // .ToReadOnlySeString(); - // - // Append(Plugin.Evaluator.Evaluate(translatePayload).ToString()); - // chunks.Add(new IconChunk(source, null, BitmapFontIcon.AutoTranslateEnd)); - // } - // break; - // case MacroCode.Icon: - // if (payload.TryGetExpression(out var eIcon) && TryResolveInt(eIcon, out var iconVal)) - // chunks.Add(new IconChunk(source, link, (BitmapFontIcon)iconVal)); - // break; - // case MacroCode.Link: - // if (!payload.TryGetExpression( - // out var linkTypeExpr1, - // out var uintExpr2, - // out var intExpr3, - // out var intExpr4, - // out var strExpr5)) - // break; - // - // if (!linkTypeExpr1.TryGetUInt(out var linkType)) - // break; - // - // switch ((LinkMacroPayloadType)linkType) - // { - // case LinkMacroPayloadType.Terminator: - // link = null; - // break; - // case LinkMacroPayloadType.MapPosition: - // if (!uintExpr2.TryGetUInt(out var ids)) - // break; - // - // if (!intExpr3.TryGetInt(out var rawX)) - // break; - // - // if (!intExpr4.TryGetInt(out var rawY)) - // break; - // - // var mapId = ids & 0xFF; - // var territoryId = (ids >> 16) & 0xFF; - // break; - // case (LinkMacroPayloadType)Payload.EmbeddedInfoType.DalamudLink - 1: - // if (!uintExpr2.TryGetUInt(out var commandId)) - // break; - // - // if (!intExpr3.TryGetInt(out var extra1)) - // break; - // - // if (!intExpr4.TryGetInt(out var extra2)) - // break; - // - // if (!strExpr5.TryGetString(out var extraStr)) - // break; - // break; - // case LinkMacroPayloadType.Quest: - // if (!uintExpr2.TryGetUInt(out var questId)) - // break; - // break; - // case LinkMacroPayloadType.Status: - // if (!uintExpr2.TryGetUInt(out var statusId)) - // break; - // break; - // case LinkMacroPayloadType.Item: - // if (!uintExpr2.TryGetUInt(out var itemId)) - // break; - // break; - // case LinkMacroPayloadType.Character: - // if (!uintExpr2.TryGetUInt(out var flags)) - // break; - // - // if (!intExpr3.TryGetUInt(out var worldId)) - // break; - // break; - // case LinkMacroPayloadType.PartyFinder: - // if (!uintExpr2.TryGetUInt(out var listingId)) - // break; - // - // // intExpr3 is unused - // - // if (!intExpr4.TryGetUInt(out worldId)) - // break; - // break; - // case LinkMacroPayloadType.PartyFinderNotification: - // // no expr used - // break; - // case LinkMacroPayloadType.Achievement: - // if (!uintExpr2.TryGetUInt(out var achievementId)) - // break; - // break; - // } - // break; - // case MacroCode.NonBreakingSpace: - // Append(" "); - // break; - // case PayloadType.Unknown: - // var rawPayload = (RawPayload)payload; - // else if (rawPayload.Data.Length > 1 && rawPayload.Data[1] == 0x14) - // { - // if (glow.Count > 0) - // { - // glow.Pop(); - // } - // else if (rawPayload.Data.Length > 6 && rawPayload.Data[2] == 0x05 && rawPayload.Data[3] == 0xF6) - // { - // var (r, g, b) = (rawPayload.Data[4], rawPayload.Data[5], rawPayload.Data[6]); - // glow.Push(ColourUtil.ComponentsToRgba(r, g, b)); - // } - // } - // break; - // } - // } - // - // return chunks; - // } - internal static IEnumerable ToChunks( SeString msg, ChunkSource source, @@ -499,47 +261,4 @@ internal static class ChunkUtil return BitConverter.ToUInt32(numArray, 0); } - - // private static bool TryResolveUInt(in ReadOnlySeExpressionSpan expression, out uint value) - // { - // if (expression.TryGetUInt(out value)) - // return true; - // - // if (expression.TryGetParameterExpression(out var exprType, out var operand1)) - // { - // if (!TryResolveUInt(operand1, out var paramIndex)) - // return false; - // - // if (paramIndex == 0) - // return false; - // - // paramIndex--; - // if ((ExpressionType)exprType == ExpressionType.GlobalNumber) - // { - // value = (uint) GlobalParametersCache.GetValue((int)paramIndex); - // return true; - // } - // // return (ExpressionType)exprType switch - // // { - // // // ExpressionType.LocalNumber => context.TryGetLNum((int)paramIndex, out value), // lnum - // // ExpressionType.GlobalNumber => (uint) GlobalParametersCache.GetValue((int)paramIndex), // gnum - // // _ => false, // gstr, lstr - // // }; - // } - // - // return false; - // } - - // [MethodImpl(MethodImplOptions.AggressiveInlining)] - // private static bool TryResolveInt(in ReadOnlySeExpressionSpan expression, out int value) - // { - // if (TryResolveUInt(expression, out var u32)) - // { - // value = (int)u32; - // return true; - // } - // - // value = 0; - // return false; - // } } diff --git a/HellionChat/Util/ColourUtil.cs b/HellionChat/Util/ColourUtil.cs index a8bd6ac..d888e09 100755 --- a/HellionChat/Util/ColourUtil.cs +++ b/HellionChat/Util/ColourUtil.cs @@ -91,7 +91,7 @@ internal static class ColourUtil } // Modulates the alpha byte of an ABGR color by a factor in [0, 1]. - // RGB stays intact. Used by the PM-3 hover-lerp path where each + // RGB stays intact. Used by the hover-lerp path where each // frame produces a fractional alpha value but the colour itself // must not shift. internal static uint ApplyAlpha(uint abgr, float alphaFactor) @@ -103,7 +103,7 @@ internal static class ColourUtil } // Mixes an ABGR colour's RGB channels toward white (0xFF) by factor t in - // [0, 1]; the alpha byte is left untouched. A1 hover-sheen accent-tint: + // [0, 1]; the alpha byte is left untouched. Hover-sheen accent tint: // a low factor nudges the sweep toward the element's accent hue without // going fully saturated (effect level stays "subtle"). RGB-only on // purpose -- DrawHoverSheen owns the alpha falloff. diff --git a/HellionChat/Util/ImGuiUtil.cs b/HellionChat/Util/ImGuiUtil.cs index db36fe0..4ed1fb3 100755 --- a/HellionChat/Util/ImGuiUtil.cs +++ b/HellionChat/Util/ImGuiUtil.cs @@ -90,10 +90,9 @@ internal static class ImGuiUtil using (ImRaii.PushColor(ImGuiCol.Text, ImGui.GetStyle().Colors[(int)ImGuiCol.TextDisabled])) ImGui.TextUnformatted("(?)"); - // AllowWhenDisabled — ohne das Flag liefert IsItemHovered bei - // ausgegrauten Settings false, der User könnte nicht mehr lesen - // warum eine Option nicht aktiv ist. Genau dann braucht er den - // Hover-Tooltip aber am dringendsten. + // AllowWhenDisabled, because without it IsItemHovered returns false on a + // greyed-out setting -- and a reader who cannot find out why an option is + // inactive is exactly the reader who needs the tooltip most. if (!ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) return; diff --git a/HellionChat/Util/SenderNameDisplay.cs b/HellionChat/Util/SenderNameDisplay.cs index 3ee9c89..3d4f959 100644 --- a/HellionChat/Util/SenderNameDisplay.cs +++ b/HellionChat/Util/SenderNameDisplay.cs @@ -4,15 +4,14 @@ using HellionChat._Helpers; namespace HellionChat.Util; -// UI-7: produces a render-only view of a chunk list with the sender's name +// Produces a render-only view of a chunk list with the sender's name // reformatted per the user's WorldSuffixMode / NameFormMode. Called from // ChatLogWindow.DrawChunks on every draw — it never mutates the input, so the // stored message (the Sender BLOB in the DB) stays byte-for-byte unchanged and // a later settings change reformats history too. // Known trade-off: at non-default settings, this allocates one List per // visible message per frame. Lists are small and the path is skipped at the -// neutral defaults, so GC pressure is low in practice. Accepted; noted in -// Cycle Notes. +// neutral defaults, so GC pressure is low in practice. Accepted as-is. internal static class SenderNameDisplay { // Returns a copy of the list with the whole ChunkSource.Sender span @@ -77,7 +76,7 @@ internal static class SenderNameDisplay // text, and any cross-world icon) with one formatted chunk that keeps // the PlayerPayload link so the name stays clickable. Dropping the // original sender icon is an accepted trade-off and only happens once - // the user moves UI-7 off its defaults. + // the user moves off its defaults. // Channel brackets and colons are ChunkSource.None wrappers outside the // Sender span — they are preserved untouched by the copy loops below. var copy = new List(chunks.Count); diff --git a/HellionChat/Util/TabLifecycleHelpers.cs b/HellionChat/Util/TabLifecycleHelpers.cs index 5c9887f..998b5c3 100644 --- a/HellionChat/Util/TabLifecycleHelpers.cs +++ b/HellionChat/Util/TabLifecycleHelpers.cs @@ -72,7 +72,7 @@ internal static class TabLifecycleHelpers public static bool ShouldStripOnSave(Tab t) => IsInUnpinnedPool(t); - // GP-04: clear every Tab.PopOut at load time. The pool binds later, so at + // Clear every Tab.PopOut at load time. The pool binds later, so at // load NO tab can own a slot — a persisted PopOut=true is always a stale flag // with no window. Unconditional (pinned included) because pinned TempTabs // survive the load and are the main stale-flag source; a !IsPinned filter @@ -91,7 +91,7 @@ internal static class TabLifecycleHelpers // partner-name label) so a normal typed line cannot route as a silent /tell // to the old partner — the same privacy guard StripTellBindingOnPromote // applies on promote. Re-activating the already-active tab must NOT strip - // (a live game-tell would lose its context, TR-4); a tab carrying its own + // (a live game-tell would lose its context); a tab carrying its own // Tab.TellTarget is a real tell binding (leg1) and is left intact. internal static void OnTabActivated(Tab tab, Tab? previous) { diff --git a/HellionChat/Util/TabsUtil.cs b/HellionChat/Util/TabsUtil.cs index 264e186..e41f9b5 100755 --- a/HellionChat/Util/TabsUtil.cs +++ b/HellionChat/Util/TabsUtil.cs @@ -97,6 +97,21 @@ public static class TabsUtil Channel = InputChannel.NoviceNetwork, }; + // Requested by a tester: emotes have their own rhythm and get lost between + // system notices. Echo rides along because /echo is where emote-adjacent + // output lands. + public static Tab HellionEmote => + new() + { + Name = HellionStrings.Tabs_Presets_Emote, + SelectedChannels = new Dictionary + { + [ChatType.CustomEmote] = (ChatSourceExt.All, ChatSourceExt.All), + [ChatType.StandardEmote] = (ChatSourceExt.All, ChatSourceExt.All), + [ChatType.Echo] = (ChatSourceExt.All, ChatSourceExt.All), + }, + }; + public static Tab HellionSystem => new() { diff --git a/HellionChat/_Helpers/PluginDisclosureScanner.cs b/HellionChat/_Helpers/PluginDisclosureScanner.cs index 5f4029e..768330f 100644 --- a/HellionChat/_Helpers/PluginDisclosureScanner.cs +++ b/HellionChat/_Helpers/PluginDisclosureScanner.cs @@ -1,6 +1,6 @@ namespace HellionChat._Helpers; -// UI-11 pure decision helper: does a message about to be sent carry a glyph +// pure decision helper: does a message about to be sent carry a glyph // that only renders correctly for players running HellionChat or a similar // plugin? Those are FFXIV Private-Use-Area icon codepoints (the same range // SeIconChar covers); a recipient without a plugin sees an empty box. diff --git a/HellionChat/_Helpers/SenderNameFormatter.cs b/HellionChat/_Helpers/SenderNameFormatter.cs index a47b11b..5637ad5 100644 --- a/HellionChat/_Helpers/SenderNameFormatter.cs +++ b/HellionChat/_Helpers/SenderNameFormatter.cs @@ -2,7 +2,7 @@ using System; namespace HellionChat._Helpers; -// UI-7 pure decision helper: builds the display string for a sender's name + +// pure decision helper: builds the display string for a sender's name + // world per the user's NameFormMode / WorldSuffixMode. Dalamud-free so the // Build Suite can cover every combination; SenderNameDisplay feeds it the name // and world it pulled from the PlayerPayload. diff --git a/README.md b/README.md index 7c9d010..be82db7 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Build](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/actions/workflows/build.yml/badge.svg?branch=main)](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/actions/workflows/build.yml) [![License: EUPL-1.2](https://img.shields.io/badge/License-EUPL--1.2-blue.svg)](LICENSE) -[![Latest release](https://img.shields.io/badge/release-v1.5.6-brightgreen)](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/latest) +[![Latest release](https://img.shields.io/badge/release-v2.0.0-brightgreen)](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/latest) [![Dalamud API](https://img.shields.io/badge/Dalamud-API_15-purple)](https://github.com/goatcorp/Dalamud) [![.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/) @@ -11,8 +11,8 @@ Hellion Forge

-**Version 1.5.6** — Privacy-first chat plugin for FINAL FANTASY XIV / Dalamud, built on -[Chat 2](https://github.com/Infiziert90/ChatTwo) (EUPL-1.2). +**Version 2.0.0** — Privacy-first chat plugin for FINAL FANTASY XIV / Dalamud, originally +forked from [Chat 2](https://github.com/Infiziert90/ChatTwo) (EUPL-1.2). Hellion Chat is a privacy-first plugin built on the Chat 2 foundation. The majority of the engine comes from Chat 2 (message store, channel logic, hook system), and most keyboard shortcuts continue @@ -301,6 +301,15 @@ An optional submission to the Dalamud main plugin repo (in addition to the custo ## Project Status +**Version 2.0.0** — Rebuilt, Repaired, Reset. Nine development cycles replaced every +window in the plugin: the sidebar, tab strip and status bar, the settings window, the +message list, the input row and the first-run wizard are drawn by the plugin rather than +by ImGui defaults, and share one visual language. Typography carries structure, anything +you can press gets a surface, and colours are measured against what sits behind them. +**This update resets your settings** — nine cycles of window rebuilds left saved values +pointing at surfaces that no longer exist. The message database is untouched and the old +config is kept alongside it as `HellionChat.json.pre-2.0.0.bak`. + **Version 1.5.6** — Settings Overhaul + Filter & Notification Polish. The Settings window has been reorganised from ten tabs to seven (General, Appearance, Chat, Window, Channels, Data & Privacy, About). Each tab now uses collapsible sections grouped by control type; diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index f7e7651..fa83b79 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -11,6 +11,48 @@ releases as an overview and links to the release pages for details. --- +## [2.0.0] — 2026-08-19 + +Rebuilt, repaired, reset. Everything developed as v1.6.0 through v1.15.0 ships +here: those versions were never published on their own, because the whole window +layer was being rewritten from ImGui defaults to custom drawing and shipping it +half-done would have been worse than shipping nothing. + +**This release resets your configuration.** Nine cycles of window rebuilds left +saved values pointing at surfaces that no longer exist, and starting over is the +only way to be sure every install is on the same defaults. The message database is +untouched — it lives in its own file. The previous config is kept alongside it as +`HellionChat.json.pre-2.0.0.bak`. + +### Added + +- An Emote tab in the default layout, requested by a tester: custom emotes, + standard emotes and echo, which is where emote-adjacent output lands. +- `/hellion wizard` reopens the first-run wizard. It had no way back into it at + all — the reopen button lost its call site in the four-step rewrite and nothing + replaced it, so the only route was closing the game and editing the config. +- The plugin notice on the welcome page: plugins are a grey area in this game and + do not belong in public channels. + +### Changed + +- The first-run wizard is drawn like the rest of the plugin. Profile cards are + tiles in the row language, sized to their own text so a privacy description is + never cut off mid-sentence, and their emoji are icon-font glyphs. +- The welcome page is opaque, unlike every other window here. It is read once and + carries a privacy decision; the game showing through it was noise. +- Config version 27 replaces the config instead of migrating it. + +### Fixed + +- The GDPR notice for the full-history profile is shown again. It had been + translated into 25 languages and drawn nowhere since 2026-05-18. +- Two profile cards claimed to be recommended at once: the badge sat on one while + the other still said "(recommended)" in its heading, in every language. +- Reopening the wizard no longer carries staged choices from a previous run. + +--- + ## [1.14.0] — unreleased (local only) The input row. It was the one surface a user actually works in and it spoke three diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 4e6b69f..27a348c 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -10,52 +10,58 @@ be a poor fit for the plugin's privacy-first scope during brainstorming. --- -## Current development (unreleased) +## Released -The published release is **v1.5.6**. Development since then runs as a UI rebuild towards v2.0.0 and -is not published: the whole window layer is being rewritten from ImGui defaults to custom drawing. -Versions v1.6.0 through v1.14.0 are local development states, and `repo.json` deliberately keeps -its download links on v1.5.6 so nobody updates into a partial state. +**v2.0.0 — Rebuilt, Repaired, Reset (2026-08-19)** ships everything that was +developed as v1.6.0 through v1.15.0. Those versions were never published on their +own: the whole window layer was being rewritten from ImGui defaults to custom +drawing, and `repo.json` deliberately kept its download links on v1.5.6 so nobody +would update into a partial state. -Where it stands: +What went into it, cycle by cycle: -- **v1.6.0 to v1.8.x** — new window layer, settings window, channel pop-outs, and the restoration - pass that brought v1.5.6 behaviour back onto it. +- **v1.6.0 to v1.8.x** — new window layer, settings window, channel pop-outs, and + the restoration pass that brought v1.5.6 behaviour back onto it. - **v1.9.0** — polish and a concurrency hardening pass. -- **v1.10.0** — the style foundation: a scale-aware layout layer, held hover state, and five drawn - widgets, applied to the sidebar, the tab strip and the status bar. -- **v1.11.0** — the settings window rebuilt on those widgets, and the defects that surfaced while - doing it: three settings with no control, a pinned tell tab that never loaded its history, six - switches that changed nothing, and dropdowns listing blank rows. Contrast is measured against the - surface now rather than taken from the theme. -- **v1.12.0** — the reconnection pass. The v1.6.0 window rebuild had left export, the tab editor, - database maintenance and pinning unreachable, and 433 of 824 translation keys with no caller. All - four features are back, the settings window is translated into 25 languages, and the channel grid - is authoritative over what gets stored. -- **v1.13.0** — typography. Named type roles instead of one size, a channel header above the - conversation, timestamps in a column of their own, and rows with a surface. Screenshot mode - reached one of four surfaces that draw a tab name and now reaches all of them. -- **v1.14.0** — the input row: two ghost buttons and a menu where five filled defaults sat, a - field with its own surface and focus rail, popups built from the sidebar's row shape, and a - softened seam between the tab list and the conversation. Plus a style lab under - `/hellion lab` and a preflight guard for the RGBA/ABGR contrast trap. -- **v1.15.0** — the first-run wizard, the last surface still drawn in ImGui defaults, and the - first thing a tester sees. +- **v1.10.0** — the style foundation: a scale-aware layout layer, held hover state, + and five drawn widgets, applied to the sidebar, the tab strip and the status bar. +- **v1.11.0** — the settings window rebuilt on those widgets, and the defects that + surfaced while doing it: three settings with no control, a pinned tell tab that + never loaded its history, six switches that changed nothing, and dropdowns + listing blank rows. Contrast is measured against the surface now rather than + taken from the theme. +- **v1.12.0** — the reconnection pass. The v1.6.0 window rebuild had left export, + the tab editor, database maintenance and pinning unreachable, and 433 of 824 + translation keys with no caller. All four features are back, the settings window + is translated into 25 languages, and the channel grid is authoritative over what + gets stored. +- **v1.13.0** — typography. Named type roles instead of one size, a channel header + above the conversation, timestamps in a column of their own, and rows with a + surface. Screenshot mode reached one of four surfaces that draw a tab name and + now reaches all of them. +- **v1.14.0** — the input row: two ghost buttons and a menu where five filled + defaults sat, a field with its own surface and focus rail, popups built from the + sidebar's row shape, and a softened seam between the tab list and the + conversation. Plus a style lab under `/hellion lab` and a preflight guard for the + RGBA/ABGR contrast trap. +- **v2.0.0** — the first-run wizard, the last surface still drawn in ImGui + defaults and the first thing a new user sees. Plus the config reset that puts + every install on the same defaults after nine cycles of rebuilding. -The sidebar is no longer on this list. Earlier drafts had it moving from tab rows to channel -rows, but both reasons for that are gone: it was flat and unstyled when the idea was written, -and it is neither now — it draws its own row surfaces and already groups pinned and auto-tell -tabs under headings. What remained was a change to how the plugin is operated rather than how -it looks, and tabs are what users have learned. They stay. - -A tester beta follows once the visual pass is complete. +The sidebar is no longer on this list. Earlier drafts had it moving from tab rows +to channel rows, but both reasons for that are gone: it was flat and unstyled when +the idea was written, and it is neither now — it draws its own row surfaces and +already groups pinned and auto-tell tabs under headings. What remained was a change +to how the plugin is operated rather than how it looks, and tabs are what users have +learned. They stay. ## After that -**v1.5.7 — Ad-Block / Spam-Filter** remains the next feature scope: the hybrid ad-block and -spam-filter cycle, combining a lightweight built-in filter with optional `NoSoliciting` -IPC integration. Plugin Integrations Wave 2-6 (Context-Menu, NotificationMaster, Moodles, -ExtraChat, XIVIM Quick-DM) follows. +**Ad-Block / Spam-Filter** is the next feature scope: a hybrid cycle combining a +lightweight built-in filter with optional `NoSoliciting` IPC integration. It was +planned as v1.5.7 before the window rebuild took priority. Plugin Integrations +Wave 2-6 (Context-Menu, NotificationMaster, Moodles, ExtraChat, XIVIM Quick-DM) +follows. Native-speaker review of the AI-assisted v1.5.3 translations (13 legacy Crowdin locales) runs in parallel as a continuous correction pass, gathered via the Hellion Forge Discord. diff --git a/repo.json b/repo.json index 51e45e2..cf8715f 100644 --- a/repo.json +++ b/repo.json @@ -1,42 +1,42 @@ [ - { - "Author": "Jon Kazama (Hellion Forge)", - "Name": "Hellion Chat", - "InternalName": "HellionChat", - "AssemblyVersion": "1.5.6.0", - "Description": "A Hellion Forge plugin — privacy-focused chat replacement for FINAL FANTASY XIV, built for EU, US and JP data rules.\n\nBy default only your own conversations are stored. Public chat, NPC dialogue, system messages and battle logs are discarded at the storage layer unless you opt in. Retention windows are configurable per channel, history can be wiped retroactively, and everything can be exported on demand.\n\nFeatures:\n- Channel whitelist with a Privacy-First default\n- Per-channel retention with a daily background sweep\n- Retroactive cleanup with preview and Ctrl+Shift confirm\n- Export to Markdown, JSON or CSV\n- First-run wizard with four profiles: Privacy-First, Casual, Roleplay, Full History\n- Multi-language UI (24 locales) with live language switching\n- Own config and database — no shared state with other plugins\n\nBased on Chat 2 by Infi and Anna (EUPL-1.2).\nSupport: https://discord.gg/X9V7Kcv5gR", - "ApplicableVersion": "any", - "RepoUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat", - "Tags": [ - "Social", - "UI", - "Chat", - "Replacement", - "Privacy" - ], - "DalamudApiLevel": 15, - "LoadRequiredState": 0, - "LoadSync": false, - "CanUnloadAsync": false, - "LoadPriority": 0, - "Punchline": "A Hellion Forge plugin. Privacy-first chat for FFXIV, built to stay out of your way.", - "Changelog": "**v1.5.6 — Settings Overhaul + Filter & Notification Polish (2026-05-23)**\n\n- Settings window reorganised: ten tabs down to seven (General, Appearance, Chat, Window, Channels, Data & Privacy, About). Each tab now uses collapsible sections grouped by control type. Sections start collapsed every time you open a tab — less noise, easier to find what you need.\n- New sender-name display options under Chat → Messages: separate world-suffix and name-format modes (Full name / First name only / Initials × Never / Other worlds only / Always).\n- Plugin-only symbols now show a pre-send warning so other players do not get empty boxes (Chat → Messages → \"Warn before sending plugin-only symbols\").\n- Separate window opacity for focused vs. inactive chat window (Appearance → Window style → \"Inactive window opacity\"). The slider above sets the focused value.\n- Custom notification sound volume slider (General → Sound, and mirrored in Channels → per-tab → Notification). Affects only the three bundled custom sounds; the 16 game sounds are unaffected.\n- The per-tab regex filter that briefly shipped earlier in this cycle has been removed — FFXIV's built-in blackword filter covers the same need.\n- All 24 locale files updated for the new section labels and the v1.5.6 control labels (machine translation; native review continues via the Hellion Forge Discord).\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\n**v1.5.5 — Upstream-Sync Tab-Features (2026-05-21)**\n\nA backlog-sync cycle: inherited tab-feature items plus a new fox\nbanner image and custom notification sounds.\n\nUser-visible:\n\n- Failed tells now raise a warning toast when a message you sent\n could not be delivered (recipient offline, in an instance, or\n blocking you). Toggle in Settings, Chat tab.\n- Per-tab notification sound: each tab can play a sound when a\n message arrives while you are looking at a different tab. Pick\n one of the 16 game chat sounds or one of three bundled Hellion\n sounds, with a preview button to hear it. Off by default,\n respects the global sound toggle.\n- The tab rename field in the right-click menu now focuses\n itself when the menu opens and accepts up to 512 characters,\n matching the settings-tab rename.\n- A jump-to-latest button appears in the chat log header while\n you are scrolled up from the live end.\n- Map flags and item links can be inserted into the chat input\n from its right-click menu.\n- The Hellion Forge fox banner in the first-run wizard and the\n Information tab is now a real image instead of ASCII art.\n\nSchema bumped to v18 (additive fields only, no data migration).\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\n**v1.5.4 — Polish and Motion (2026-05-20)**\n\nA polish cycle: smoother theme switching, faster theme and tab\naccess, and subtle hover motion. Three P3 items plus an\naccessibility toggle.\n\nUser-visible:\n\n- Theme switches now crossfade smoothly over ~300 ms across every\n Hellion-rendered surface — sidebar, title, buttons, tabs,\n scrollbar, separators. The window background snaps deliberately\n so the per-window opacity override from Dalamud's pinning menu\n stays untouched.\n- New header quick-picker: a palette button left of the cog opens\n a compact popup with two sections — every built-in and custom\n theme, and every tab. The active entry carries a check glyph;\n clicking another switches without closing the popup.\n- Sidebar icons ease their opacity on hover, and card-mode message\n borders highlight per tab while the cursor is over their rows.\n Framerate-independent, so a stalled Wine frame cannot overshoot\n the animation.\n- New \"Reduce motion\" toggle in Theme & Layout disables the\n crossfade, the hover animations and the unread-tab pulse for\n users who prefer a static UI.\n\nUnder the hood:\n\n- Two pure-helper lerp paths (ThemeAbgrCacheLerp, FrameLerp) with\n xUnit coverage in the Build Suite, plus a ColourUtil.ApplyAlpha\n alpha modulator. Two new /xlperf self-test steps pin the\n crossfade and quick-picker contracts.\n\nNo schema bump, no migration. Migration v17 stays.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\n**v1.5.3 — Localisation Wave + Bundled-Font Overhaul (2026-05-19)**\n\nMulti-language pass plus a long-standing first-frame HITCH lands\nas a side effect of a font-stack rewrite.\n\nUser-visible:\n\n- 24 selectable UI languages (was 2). Catalan, Czech, Danish,\n Dutch, English, Finnish, French, German, Greek, Hungarian,\n Italian, Japanese, Korean, Norsk bokmål, Polish, Portuguese\n (BR + PT), Romanian, Russian, Spanish, Swedish, Turkish,\n Ukrainian, Simplified + Traditional Chinese. Sorted by endonym,\n \"None\" pinned first. Non-native locales are AI-assisted and\n flagged for native-speaker review via the Forge Discord.\n- Bundled Inter Light replaces Exo 2 (SIL OFL 1.1, 343 KB). The\n Inter font ships Latin Extended-A/B, Greek polytonic and\n Cyrillic Supplement coverage; NotoSansCjkRegular joins as a\n third merge layer for Hangul and Simplified-Han glyphs the\n FFXIV Japanese game font does not ship.\n- First-frame HITCH dropped from ~74 ms (v1.5.2 baseline that\n held since v1.4.x) to a median of ~20 ms (5-reload sample\n 17.9-23.6 ms, Linux/Wine). The bundled-font path silently\n fell back to the FFXIV Axis font for the entire v1.5.x series\n because of an early-return in the draw loop. The fix that\n routes RegularFont through draw also lands the defer-pattern\n win the v1.5.1 cycle was reaching for.\n- ExtraGlyphRanges auto-activates on language change. Korean,\n ChineseFull and the two new flags (LatinExtended, Greek) toggle\n on without a manual visit to Fonts and Colours.\n- New WarningText under the language dropdown notes FFXIV's\n chat input only fully supports EN/DE/FR/JA character sets.\n Other languages render in HellionChat but may garble when\n typed into in-game chat.\n\nUnder the hood:\n\n- Three-layer font stack: Inter Light primary, FFXIV\n JapaneseFont merge 1 for kana/kanji style, NotoSansCjkRegular\n merge 2 for everything else CJK.\n- LanguageOverride enum gains ten locales plus three previously\n commented out (Italian, Korean, Norwegian as `nb`). New\n values append to the enum so existing config integers stay\n stable across update.\n- Crowdin gap closed: four post-sync ChatTwo keys backfilled\n into 13 legacy locales with per-key AI markers.\n- Plugin.LoadAsync runs a one-shot migration that ORs in the\n matching ExtraGlyphRanges flag for users already on a\n non-default language. Settings.Apply auto-activates on\n change going forward.\n- Em-dash sweep across the EN source and 18 translations to the\n house style. Russian and Ukrainian keep the typographic norm.\n\nMigration v17 stays. UseHellionFont users transition from Exo 2\nto Inter Light transparently on first reload.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\nFull history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases", - "AcceptsFeedback": true, - "DownloadLinkInstall": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.5.6/latest.zip", - "DownloadLinkUpdate": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.5.6/latest.zip", - "DownloadLinkTesting": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.5.6/latest.zip", - "TestingAssemblyVersion": "1.5.6.0", - "IconUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png", - "ImageUrls": [ - "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/chatWindow.png", - "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/settingsOverview.png", - "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/themesPicker.png" - ], - "DownloadCount": 0, - "IsHide": false, - "IsTestingExclusive": false, - "CategoryTags": [ - "social" - ] - } + { + "Author": "Jon Kazama (Hellion Forge)", + "Name": "Hellion Chat", + "InternalName": "HellionChat", + "AssemblyVersion": "2.0.0.0", + "Description": "A Hellion Forge plugin — privacy-focused chat replacement for FINAL FANTASY XIV, built for EU, US and JP data rules.\n\nBy default only your own conversations are stored. Public chat, NPC dialogue, system messages and battle logs are discarded at the storage layer unless you opt in. Retention windows are configurable per channel, history can be wiped retroactively, and everything can be exported on demand.\n\nFeatures:\n- Channel whitelist with a Privacy-First default\n- Per-channel retention with a daily background sweep\n- Retroactive cleanup with preview and Ctrl+Shift confirm\n- Export to Markdown, JSON or CSV\n- First-run wizard with four profiles: Privacy-First, Casual, Roleplay, Full History\n- Multi-language UI (24 locales) with live language switching\n- Own config and database — no shared state with other plugins\n\nBased on Chat 2 by Infi and Anna (EUPL-1.2).\nSupport: https://discord.gg/X9V7Kcv5gR", + "ApplicableVersion": "any", + "RepoUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat", + "Tags": [ + "Social", + "UI", + "Chat", + "Replacement", + "Privacy" + ], + "DalamudApiLevel": 15, + "LoadRequiredState": 0, + "LoadSync": false, + "CanUnloadAsync": false, + "LoadPriority": 0, + "Punchline": "A Hellion Forge plugin. Privacy-first chat for FFXIV, built to stay out of your way.", + "Changelog": "**v2.0.0 — Rebuilt, Repaired, Reset (2026-08-19)**\n\nNine development cycles in one release. Everything built as v1.6.0 through v1.15.0 ships here; those versions were never published on their own.\n\n**This update resets your settings.** Nine cycles of rebuilding left saved values pointing at surfaces that no longer exist, and starting over is the only way to be sure every install is on the same defaults. **Your message history is untouched** — it lives in a separate database. Your old settings are kept next to the config file as `HellionChat.json.pre-2.0.0.bak`.\n\nFixed, and several of these lost data or hid it:\n\n- Retroactive cleanup could never be applied at all. The preview took the database lock itself and that counted as a change, so every preview went stale the instant it finished and the apply button never appeared.\n- Compacting the database ran against an open reader and failed, after which the plugin reported that nothing had been deleted — while everything had.\n- Deleting messages left them in the search index, so search kept returning rows that were already gone.\n- Pinned tell tabs came up empty for a whole session: the history query ran at plugin start, before any character is logged in, and never tried again.\n- Export wrote invalid JSON where a setting value was involved, and a byte-order mark that strict parsers reject.\n- A pop-out with its title bar switched on could not be closed, and a tell from a popped-out partner hijacked the main window's active tab.\n- One third-party emote service started returning 403, and that response took all 65 working global emotes down with it on every start.\n- The GDPR notice for the full-history profile had been translated into 25 languages and shown nowhere since May.\n\nChanged, and one of these changes what gets stored:\n\n- **The channel grid is now authoritative.** Until now the unknown-channel failsafe was applied to known channels too, so a channel you had unticked was still being written while that failsafe was on. If that was you, this release stores less than before. Nothing already in the database is touched.\n- Every window is drawn by the plugin rather than by ImGui defaults, and they share one visual language: structure carried by typography, a surface on anything you can press, and colours measured against what sits behind them.\n- Typography has named roles — sender, body and timestamp mean the same thing everywhere, timestamps sit in their own column, system messages are italic.\n- Export, the tab editor, database maintenance and pinning had lost their entry points during the rebuild and are reachable again.\n- Screenshot mode reached one of four surfaces that draw a tab name. It reaches all four now.\n\nRemoved: six settings that had a control and a saved value but no reader anywhere in the plugin; a per-tab regex filter the game's own blackword filter covers; and a wizard checkbox that was collected, reported as applied and never read.\n\nNew: an Emote tab in the default layout, local and server clocks in the status bar, a screenshot mode reachable from the input row, `/hellion wizard` to reopen the setup wizard, a style lab under `/hellion lab`, and 25 UI languages with the settings window and wizard fully covered.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2). The two codebases have diverged far enough that they no longer line up.", + "AcceptsFeedback": true, + "DownloadLinkInstall": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v2.0.0/latest.zip", + "DownloadLinkUpdate": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v2.0.0/latest.zip", + "DownloadLinkTesting": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v2.0.0/latest.zip", + "TestingAssemblyVersion": "2.0.0.0", + "IconUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png", + "ImageUrls": [ + "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/chatWindow.png", + "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/settingsOverview.png", + "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/themesPicker.png" + ], + "DownloadCount": 0, + "IsHide": false, + "IsTestingExclusive": false, + "CategoryTags": [ + "social" + ] + } ]