From f56b96876844ce992922329ad165b80104b03791 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Mon, 18 May 2026 19:02:54 +0200 Subject: [PATCH 01/10] feat(privacy): add Roleplay profile defaults to PrivacyDefaults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds RoleplayWhitelist (PrivacyFirst + Say + both emote types) and RoleplayRetentionOverrides (Say 30d, emotes 90d). Shout/Yell and Novice Network stay out — public-distance noise from strangers is not story content. Whitelist + overrides are IReadOnlySet / IReadOnlyDictionary with pure-helper type footprint, so the Build Suite can pin them without touching Dalamud. --- HellionChat/Privacy/PrivacyDefaults.cs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/HellionChat/Privacy/PrivacyDefaults.cs b/HellionChat/Privacy/PrivacyDefaults.cs index ae5f569..22f5c17 100644 --- a/HellionChat/Privacy/PrivacyDefaults.cs +++ b/HellionChat/Privacy/PrivacyDefaults.cs @@ -114,4 +114,29 @@ internal static class PrivacyDefaults [ChatType.StandardEmote] = 1, [ChatType.NoviceNetwork] = 1, }; + + // Roleplay: Privacy-First + Say + both emote types. Public-distance + // channels (Shout, Yell) stay out — they are public-noise from + // strangers, not story content. Novice Network also stays out; + // it is not RP-adjacent and would dilute the profile's intent. + internal static readonly IReadOnlySet RoleplayWhitelist = new HashSet( + PrivacyFirstWhitelist + ) + { + ChatType.Say, + ChatType.CustomEmote, + ChatType.StandardEmote, + }; + + // RP sessions function as story logs: Say + emotes need a longer + // window than Casual's 1-day public-chat window. 30 days for Say + // keeps in-character dialogue scrollable across multiple sessions, + // 90 days for emotes mirrors the Privacy-First conversation default. + internal static readonly IReadOnlyDictionary RoleplayRetentionOverrides = + new Dictionary + { + [ChatType.Say] = 30, + [ChatType.CustomEmote] = 90, + [ChatType.StandardEmote] = 90, + }; } From de86084dbc5b1ed481b6a19f1fa1192c9a0167f7 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Mon, 18 May 2026 20:26:22 +0200 Subject: [PATCH 02/10] feat(resources): add multi-step wizard strings for v1.5.2 (EN + DE) Thirty-two new bilingual resource keys covering all four wizard steps: titles, section headings, control labels, navigation, the new Roleplay profile, the staged-summary template strings, the 'Decide later' multi-step skip label plus its dedicated tooltip. Existing Wizard_Cancel_Label and Wizard_Cancel_Tooltip stay untouched for legacy reopen paths. --- .../Resources/HellionStrings.Designer.cs | 32 +++++++ HellionChat/Resources/HellionStrings.de.resx | 96 +++++++++++++++++++ HellionChat/Resources/HellionStrings.resx | 96 +++++++++++++++++++ 3 files changed, 224 insertions(+) diff --git a/HellionChat/Resources/HellionStrings.Designer.cs b/HellionChat/Resources/HellionStrings.Designer.cs index 559a21a..3f9311e 100644 --- a/HellionChat/Resources/HellionStrings.Designer.cs +++ b/HellionChat/Resources/HellionStrings.Designer.cs @@ -116,6 +116,38 @@ internal class HellionStrings internal static string Wizard_Reopen_Button => Get(nameof(Wizard_Reopen_Button)); internal static string Wizard_Cancel_Label => Get(nameof(Wizard_Cancel_Label)); internal static string Wizard_Cancel_Tooltip => Get(nameof(Wizard_Cancel_Tooltip)); + 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_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_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)); + internal static string Wizard_Nav_Back => Get(nameof(Wizard_Nav_Back)); + internal static string Wizard_Nav_Next => Get(nameof(Wizard_Nav_Next)); + internal static string Wizard_Nav_Finish => Get(nameof(Wizard_Nav_Finish)); + internal static string Wizard_Step3_Title => Get(nameof(Wizard_Step3_Title)); + internal static string Wizard_Step3_Section_History => Get(nameof(Wizard_Step3_Section_History)); + internal static string Wizard_Step3_Section_TellTabs => Get(nameof(Wizard_Step3_Section_TellTabs)); + internal static string Wizard_Step3_Section_Visual => Get(nameof(Wizard_Step3_Section_Visual)); + internal static string Wizard_Step3_LoadPreviousSession_Label => Get(nameof(Wizard_Step3_LoadPreviousSession_Label)); + internal static string Wizard_Step3_FilterIncludePreviousSessions_Label => Get(nameof(Wizard_Step3_FilterIncludePreviousSessions_Label)); + internal static string Wizard_Step3_AutoTellTabsHistoryPreload_Label => Get(nameof(Wizard_Step3_AutoTellTabsHistoryPreload_Label)); + internal static string Wizard_Step3_UseCompactDensity_Label => Get(nameof(Wizard_Step3_UseCompactDensity_Label)); + internal static string Wizard_Step3_PrettierTimestamps_Label => Get(nameof(Wizard_Step3_PrettierTimestamps_Label)); + internal static string Wizard_Step3_Theme_Label => Get(nameof(Wizard_Step3_Theme_Label)); + internal static string Wizard_Step4_Title => Get(nameof(Wizard_Step4_Title)); + internal static string Wizard_Step4_SummaryHeading => Get(nameof(Wizard_Step4_SummaryHeading)); + internal static string Wizard_Step4_Summary_Profile => Get(nameof(Wizard_Step4_Summary_Profile)); + internal static string Wizard_Step4_Summary_History => Get(nameof(Wizard_Step4_Summary_History)); + internal static string Wizard_Step4_Summary_TellTabs => Get(nameof(Wizard_Step4_Summary_TellTabs)); + internal static string Wizard_Step4_Summary_Visual => Get(nameof(Wizard_Step4_Summary_Visual)); + internal static string Wizard_Step4_Summary_Unchanged => Get(nameof(Wizard_Step4_Summary_Unchanged)); + internal static string Wizard_Step4_TestHint => Get(nameof(Wizard_Step4_TestHint)); + internal static string Wizard_Step4_SettingsHint => Get(nameof(Wizard_Step4_SettingsHint)); internal static string Export_Heading => Get(nameof(Export_Heading)); internal static string Export_Help => Get(nameof(Export_Help)); diff --git a/HellionChat/Resources/HellionStrings.de.resx b/HellionChat/Resources/HellionStrings.de.resx index 30d76f1..65b1a3b 100644 --- a/HellionChat/Resources/HellionStrings.de.resx +++ b/HellionChat/Resources/HellionStrings.de.resx @@ -228,6 +228,102 @@ Schließt den Wizard ohne Profil-Auswahl. Die Plugin-Defaults bleiben aktiv und der Wizard erscheint beim nächsten Plugin-Reload erneut. + + Willkommen bei Hellion Chat + + + Ein Chat 2 Fork von Hellion Forge mit DSGVO-konformen Defaults, brand-konsistentem Look und Quality-of-Life-Verbesserungen. + + + 3 kurze Schritte. Du kannst alles später unter Einstellungen → Hellion Chat ändern. + + + Später entscheiden + + + Assistenten schließen. Die Plugin-Standardwerte bleiben aktiv. Du kannst den Assistenten über Einstellungen → Hellion Chat erneut öffnen. + + + Was darf gespeichert werden? + + + ★ = empfohlen für die meisten Spieler. + + + Roleplay + + + Wie Datensparsamkeit, plus Sagen und beide Emote-Typen für deine Story-Logs. Schreien und Rufen bleiben außen vor — Public-Distance-Lärm von Fremden ist kein Story-Inhalt. Aufbewahrung: Sagen 30 Tage, Emotes 90 Tage. + + + Roleplay übernehmen + + + ‹ Zurück + + + Weiter › + + + Fertig ✓ + + + Versteckte Defaults + + + Verlauf + + + Tell-Tabs + + + Optik + + + Vorherige Session beim Start laden + + + Filter auch auf alte Messages anwenden + + + N Tell-Messages beim Öffnen eines Auto-Tabs vorladen + + + Kompakter Density-Modus + + + Schönere Timestamps (relative Zeit) + + + Theme + + + Du bist startklar + + + Deine Konfiguration + + + Profil: {0} + + + Verlauf: {0} + + + Tell-Tabs: {0} Messages vorladen + + + Optik: {0} + + + (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. + + + Einstellungen → Hellion Chat zum späteren Anpassen + Export (DSGVO Art. 15 — Auskunftsrecht) diff --git a/HellionChat/Resources/HellionStrings.resx b/HellionChat/Resources/HellionStrings.resx index 9de7345..e264abd 100644 --- a/HellionChat/Resources/HellionStrings.resx +++ b/HellionChat/Resources/HellionStrings.resx @@ -228,6 +228,102 @@ Close the wizard without selecting a profile. The plugin defaults stay active and the wizard returns on next plugin load. + + 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. + + + Three short steps. You can change everything later under Settings → Hellion Chat. + + + Decide later + + + Close the wizard. The plugin defaults stay active. You can reopen the wizard from Settings → Hellion Chat. + + + What gets stored? + + + ★ = recommended for most players. + + + Roleplay + + + Like Privacy First, plus Say and both emote types for your story logs. Shout and Yell stay out — public-distance noise from strangers is not story content. Retention: Say 30 days, emotes 90 days. + + + Apply roleplay + + + ‹ Back + + + Next › + + + Finish ✓ + + + Hidden defaults + + + History + + + Tell tabs + + + Visual + + + Load previous session on startup + + + Apply filters to messages from previous sessions + + + Preload N tell messages when an auto-tab opens + + + Compact density + + + Prettier timestamps (relative time) + + + Theme + + + You're all set + + + Your configuration + + + Profile: {0} + + + History: {0} + + + Tell tabs: preload {0} messages + + + Visual: {0} + + + (unchanged) + + + 💡 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 + Export (GDPR Art. 15 — Right of access) From 2cc260170ec7fa0284b4d2406e15b3af9b096e81 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Mon, 18 May 2026 21:15:27 +0200 Subject: [PATCH 03/10] feat(ui): rewrite FirstRunWizard as four-step staged-commit flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Multi-step navigation (Welcome → Privacy → Power Settings → Done) with a nested WizardState holding nullable Pending* fields. Profile picker becomes a 2x2 grid covering all four privacy profiles (PrivacyFirst, Casual ★ recommended, Roleplay new, FullHistory). Power-settings step surfaces six previously-hidden Configuration fields (LoadPreviousSession, FilterIncludePreviousSessions, AutoTellTabsHistoryPreload, UseCompactDensity, PrettierTimestamps, Theme) without introducing new ones. ApplyRoleplay mirrors the existing Apply* methods, CommitPending writes only the non-null fields back so skipping a step preserves existing config. OnClose docstring updated to reflect the actual code path (both Decide-Later and Finish set FirstRunCompleted = true, the wizard does not reopen). --- HellionChat/Ui/FirstRunWizard.cs | 528 +++++++++++++++++++++++++------ 1 file changed, 428 insertions(+), 100 deletions(-) diff --git a/HellionChat/Ui/FirstRunWizard.cs b/HellionChat/Ui/FirstRunWizard.cs index f54b4af..3fb46c5 100644 --- a/HellionChat/Ui/FirstRunWizard.cs +++ b/HellionChat/Ui/FirstRunWizard.cs @@ -1,3 +1,4 @@ +using System.Globalization; using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Utility.Raii; @@ -6,13 +7,29 @@ using HellionChat.Branding; using HellionChat.Code; using HellionChat.Privacy; using HellionChat.Resources; +using HellionChat.Themes; using HellionChat.Util; namespace HellionChat.Ui; +// Multi-step first-run wizard. public sealed because Plugin.cs has a +// public-typed property on this class — narrowing to internal would +// be a build break across the assembly boundary. State lives in a +// 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. 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 WizardState _state = new(); internal FirstRunWizard(Plugin plugin) : base($"{HellionStrings.Wizard_Title}###hellion-firstrun") @@ -32,138 +49,407 @@ public sealed class FirstRunWizard : Window public override void OnClose() { // OnClose fires on explicit X-click and on plugin dispose. We never - // implicitly accept the defaults here — the explicit "Later" button - // does that. If the user hasn't picked a profile yet, the wizard - // reopens on the next plugin load. + // implicitly accept the defaults here — both the explicit "Decide + // later" footer link and a successful "Finish ✓" set FirstRunCompleted + // = true, so the wizard does not reopen on the next plugin load + // regardless of which path the user took. } public override void Draw() { - DrawHellionForgeAnchor(); + DrawPagination(); ImGui.Spacing(); ImGui.Separator(); ImGui.Spacing(); - ImGui.TextWrapped(HellionStrings.Wizard_Intro); - ImGui.Spacing(); - ImGui.Separator(); - ImGui.Spacing(); - - var avail = ImGui.GetContentRegionAvail(); - var cardWidth = (avail.X - ImGui.GetStyle().ItemSpacing.X * 2) / 3f; - // Reserve room for the footer separator + cancel button below the cards. - var footerReserve = - ImGui.GetStyle().ItemSpacing.Y * 3 - + ImGui.GetTextLineHeight() - + ImGui.GetFrameHeightWithSpacing(); - var cardHeight = avail.Y - footerReserve; - - DrawCard( - "privacy-first", - cardWidth, - cardHeight, - HellionStrings.Wizard_Profile_PrivacyFirst_Heading, - HellionStrings.Wizard_Profile_PrivacyFirst_Description, - null, - HellionStrings.Wizard_Profile_PrivacyFirst_Apply, - ApplyPrivacyFirst - ); - - ImGui.SameLine(); - - DrawCard( - "casual", - cardWidth, - cardHeight, - HellionStrings.Wizard_Profile_Casual_Heading, - HellionStrings.Wizard_Profile_Casual_Description, - null, - HellionStrings.Wizard_Profile_Casual_Apply, - ApplyCasual - ); - - ImGui.SameLine(); - - DrawCard( - "full-history", - cardWidth, - cardHeight, - HellionStrings.Wizard_Profile_FullHistory_Heading, - HellionStrings.Wizard_Profile_FullHistory_Description, - HellionStrings.Wizard_Profile_FullHistory_GdprWarning, - HellionStrings.Wizard_Profile_FullHistory_Apply, - ApplyFullHistory - ); - - ImGui.Spacing(); - ImGui.Separator(); - ImGui.Spacing(); - - if (ImGui.Button(HellionStrings.Wizard_Cancel_Label)) + switch (_state.CurrentStep) { - Plugin.Config.FirstRunCompleted = true; - Plugin.SaveConfig(); - IsOpen = false; + case 1: DrawStepWelcome(); break; + case 2: DrawStepPrivacy(); break; + case 3: DrawStepPowerSettings(); break; + case 4: DrawStepDone(); break; + default: _state.CurrentStep = 1; DrawStepWelcome(); break; } - - if (ImGui.IsItemHovered()) - ImGuiUtil.Tooltip(HellionStrings.Wizard_Cancel_Tooltip); } - private void DrawCard( - string id, - float width, - float height, - string heading, - string description, - string? warning, - string buttonLabel, - Action onApply - ) + private void DrawPagination() { - using var child = ImRaii.Child($"##wizard-card-{id}", new Vector2(width, height), true); + var draw = ImGui.GetWindowDrawList(); + var avail = ImGui.GetContentRegionAvail(); + var cursor = ImGui.GetCursorScreenPos(); + const float radius = 5f; + const float spacing = 16f; + var totalWidth = (TotalSteps - 1) * spacing; + var startX = cursor.X + avail.X - totalWidth - radius; + + 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); + } + + // Reserve vertical space the circles consumed so the next widget starts below them. + ImGui.Dummy(new Vector2(0, radius * 2)); + } + + private void DrawFooter(bool showBack, bool showSkip, string primaryLabel, Action onPrimary) + { + var spacing = ImGui.GetStyle().ItemSpacing.Y; + var primaryWidth = ImGui.CalcTextSize(primaryLabel).X + ImGui.GetStyle().FramePadding.X * 2 + 16f; + var avail = ImGui.GetContentRegionAvail(); + + // Push the footer to the bottom of the window so step contents + // above can size themselves with GetContentRegionAvail(). + var lineHeight = ImGui.GetFrameHeightWithSpacing(); + var pushDown = avail.Y - lineHeight - spacing; + if (pushDown > 0) + ImGui.Dummy(new Vector2(0, pushDown)); + + ImGui.Separator(); + ImGui.Spacing(); + + if (showBack) + { + if (ImGui.Button(HellionStrings.Wizard_Nav_Back)) + _state.CurrentStep = Math.Max(1, _state.CurrentStep - 1); + ImGui.SameLine(); + } + + if (showSkip) + { + if (ImGui.Button(HellionStrings.Wizard_Step1_Skip_Label)) + { + // Skip path = matches today's Cancel path: mark first-run + // complete, save, close. No CommitPending — the user said + // 'decide later', so existing config stays as-is. + Plugin.Config.FirstRunCompleted = true; + Plugin.SaveConfig(); + IsOpen = false; + } + if (ImGui.IsItemHovered()) + ImGuiUtil.Tooltip(HellionStrings.Wizard_Step1_Skip_Tooltip); + } + + // Right-align the primary action button. + var rightX = ImGui.GetCursorPosX() + ImGui.GetContentRegionAvail().X - primaryWidth; + 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(); + } + } + + private void DrawStepWelcome() + { + ImGui.TextUnformatted(HellionStrings.Wizard_Step1_Title); + ImGui.Spacing(); + + using (Plugin.Interface.UiBuilder.MonoFontHandle.Push()) + { + // Centre the banner across the available region. Spec Z.96 + // calls for a zentrierten Mono-Font-Block. CalcTextSize must + // run inside the MonoFont push so the measurement matches the + // glyph width actually used for rendering. + var bannerSize = ImGui.CalcTextSize(HellionForgeAscii.FoxBanner); + ImGui.SetCursorPosX((ImGui.GetContentRegionAvail().X - bannerSize.X) * 0.5f); + ImGui.TextUnformatted(HellionForgeAscii.FoxBanner); + } + + ImGui.Spacing(); + ImGui.TextWrapped(HellionStrings.Wizard_Step1_Subtitle); + ImGui.Spacing(); + ImGui.TextWrapped(HellionStrings.Wizard_Step1_Footer_Hint); + + DrawFooter(showBack: false, showSkip: true, HellionStrings.Wizard_Nav_Next, + () => _state.CurrentStep = 2); + } + + 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; + + // 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); + + DrawFooter(showBack: true, showSkip: true, HellionStrings.Wizard_Nav_Next, + () => _state.CurrentStep = 3); + } + + private void DrawProfileCard(PrivacyProfile profile, string emoji, string heading, string description, bool recommended, 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); + + using var _border = ImRaii.PushColor(ImGuiCol.Border, borderColor); + using var child = ImRaii.Child($"##profile-card-{profile}", new Vector2(width, height), true); if (!child.Success) return; - ImGui.TextUnformatted(heading); + // 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; + + ImGui.SetCursorScreenPos(startPos); + + ImGui.TextUnformatted($"{emoji} {heading}{(recommended ? " ★" : string.Empty)}"); + ImGui.Spacing(); + ImGui.Separator(); + ImGui.Spacing(); + ImGui.TextWrapped(description); + } + + private void DrawStepPowerSettings() + { + // Seed only the two recommendation fields here. Other fields remain + // null until the user touches the corresponding control. + // Spec FR-4: the wizard explicitly recommends LoadPreviousSession = + // true and FilterIncludePreviousSessions = true (Config defaults are + // 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. + _state.PendingLoadPreviousSession ??= true; + _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); + + var loadPrev = _state.PendingLoadPreviousSession ?? true; + if (ImGui.Checkbox(HellionStrings.Wizard_Step3_LoadPreviousSession_Label, ref loadPrev)) + { + _state.PendingLoadPreviousSession = loadPrev; + // Mirror the DataManagement coupling: turning load-previous on + // also turns filter-include on (otherwise old messages bypass + // the filter chain), and turning filter-include off forces + // load-previous off. Same idiom as Ui/SettingsTabs/DataManagement.cs:182-200. + if (loadPrev) + _state.PendingFilterIncludePreviousSessions = true; + } + + var filterPrev = _state.PendingFilterIncludePreviousSessions ?? true; + if (ImGui.Checkbox(HellionStrings.Wizard_Step3_FilterIncludePreviousSessions_Label, ref filterPrev)) + { + _state.PendingFilterIncludePreviousSessions = filterPrev; + if (!filterPrev) + _state.PendingLoadPreviousSession = false; + } + + 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; + + var pretty = _state.PendingPrettierTimestamps ?? Plugin.Config.PrettierTimestamps; + if (ImGui.Checkbox(HellionStrings.Wizard_Step3_PrettierTimestamps_Label, ref pretty)) + _state.PendingPrettierTimestamps = pretty; + + // 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(); + } + } + } + + DrawFooter(showBack: true, showSkip: true, HellionStrings.Wizard_Nav_Next, + () => _state.CurrentStep = 4); + } + + private void DrawStepDone() + { + ImGui.TextUnformatted(HellionStrings.Wizard_Step4_Title); + ImGui.Spacing(); + + // ✓ symbol, centred-ish via dummy padding. + var checkmark = "✓"; + var checkSize = ImGui.CalcTextSize(checkmark); + var avail = ImGui.GetContentRegionAvail(); + ImGui.Dummy(new Vector2((avail.X - checkSize.X) * 0.5f, 0)); + ImGui.SameLine(); + using (ImRaii.PushColor(ImGuiCol.Text, ForgeBronze)) + ImGui.TextUnformatted(checkmark); + ImGui.Spacing(); ImGui.Separator(); ImGui.Spacing(); - ImGui.TextWrapped(description); - - if (warning is not null) + // Summary card. + using (var summary = ImRaii.Child("##wizard-summary", new Vector2(-1, 130), true)) { - ImGui.Spacing(); - ImGuiUtil.WarningText(warning); + if (summary.Success) + { + ImGui.TextUnformatted(HellionStrings.Wizard_Step4_SummaryHeading); + ImGui.Spacing(); + ImGui.Separator(); + ImGui.Spacing(); + + var profileLabel = _state.PendingProfile switch + { + PrivacyProfile.PrivacyFirst => HellionStrings.Wizard_Profile_PrivacyFirst_Heading, + PrivacyProfile.Casual => HellionStrings.Wizard_Profile_Casual_Heading, + PrivacyProfile.Roleplay => HellionStrings.Wizard_Profile_Roleplay_Heading, + PrivacyProfile.FullHistory => HellionStrings.Wizard_Profile_FullHistory_Heading, + _ => HellionStrings.Wizard_Step4_Summary_Unchanged, + }; + ImGui.TextWrapped(string.Format(HellionStrings.Wizard_Step4_Summary_Profile, profileLabel)); + + var historyLabel = (_state.PendingLoadPreviousSession ?? false) + ? HellionStrings.Wizard_Step3_LoadPreviousSession_Label + : HellionStrings.Wizard_Step4_Summary_Unchanged; + ImGui.TextWrapped(string.Format(HellionStrings.Wizard_Step4_Summary_History, historyLabel)); + + var preloadValue = _state.PendingAutoTellTabsHistoryPreload ?? Plugin.Config.AutoTellTabsHistoryPreload; + ImGui.TextWrapped(string.Format(HellionStrings.Wizard_Step4_Summary_TellTabs, preloadValue)); + + var compact = _state.PendingUseCompactDensity ?? Plugin.Config.UseCompactDensity; + var pretty = _state.PendingPrettierTimestamps ?? Plugin.Config.PrettierTimestamps; + var themeSlug = _state.PendingTheme ?? Plugin.Config.Theme; + var themeName = Plugin.ThemeRegistry.Get(themeSlug).Name; + var visualParts = new List(); + if (compact) visualParts.Add(HellionStrings.Wizard_Step3_UseCompactDensity_Label); + if (pretty) visualParts.Add(HellionStrings.Wizard_Step3_PrettierTimestamps_Label); + visualParts.Add(themeName); + ImGui.TextWrapped(string.Format(HellionStrings.Wizard_Step4_Summary_Visual, string.Join(", ", visualParts))); + } } - // Push the button to the bottom of the card. - var lineHeight = ImGui.GetFrameHeightWithSpacing(); - var remaining = ImGui.GetContentRegionAvail().Y - lineHeight; - if (remaining > 0) - ImGui.Dummy(new Vector2(0, remaining)); + ImGui.Spacing(); - if (ImGui.Button($"{buttonLabel}##{id}", new Vector2(-1, 0))) + // Inline FR-3 hint with placeholder for preload count. + var preloadForHint = _state.PendingAutoTellTabsHistoryPreload ?? Plugin.Config.AutoTellTabsHistoryPreload; + using (ImRaii.PushColor(ImGuiCol.Text, ForgeBronze)) + ImGui.TextWrapped(string.Format(HellionStrings.Wizard_Step4_TestHint, preloadForHint)); + + ImGui.Spacing(); + ImGui.TextDisabled(HellionStrings.Wizard_Step4_SettingsHint); + + DrawFooter(showBack: true, showSkip: false, HellionStrings.Wizard_Nav_Finish, () => { - onApply(); + CommitPending(); Plugin.Config.FirstRunCompleted = true; Plugin.SaveConfig(); IsOpen = false; - } + }); } - // Collapsible because the full silhouette is taller than the wizard - // window — folded by default so the privacy cards stay the primary - // focus, expandable for whoever wants the "about the makers" anchor. - private void DrawHellionForgeAnchor() + // Writes only non-null pending values back to Config. A null pending + // means the user did not touch that step's control, so the existing + // Config value is preserved. Theme switch goes through ThemeRegistry + // so the active palette updates live for the rest of the session. + internal void CommitPending() { - using var tree = ImRaii.TreeNode("Hellion Forge"); - if (!tree.Success) - return; + switch (_state.PendingProfile) + { + case PrivacyProfile.PrivacyFirst: ApplyPrivacyFirst(); break; + case PrivacyProfile.Casual: ApplyCasual(); break; + case PrivacyProfile.Roleplay: ApplyRoleplay(); break; + case PrivacyProfile.FullHistory: ApplyFullHistory(); break; + } - using (Plugin.Interface.UiBuilder.MonoFontHandle.Push()) - ImGui.TextUnformatted(HellionForgeAscii.FoxBanner); + if (_state.PendingLoadPreviousSession.HasValue) + Plugin.Config.LoadPreviousSession = _state.PendingLoadPreviousSession.Value; + + if (_state.PendingFilterIncludePreviousSessions.HasValue) + Plugin.Config.FilterIncludePreviousSessions = _state.PendingFilterIncludePreviousSessions.Value; + + if (_state.PendingAutoTellTabsHistoryPreload.HasValue) + Plugin.Config.AutoTellTabsHistoryPreload = _state.PendingAutoTellTabsHistoryPreload.Value; + + if (_state.PendingUseCompactDensity.HasValue) + Plugin.Config.UseCompactDensity = _state.PendingUseCompactDensity.Value; + + if (_state.PendingPrettierTimestamps.HasValue) + Plugin.Config.PrettierTimestamps = _state.PendingPrettierTimestamps.Value; + + if (!string.IsNullOrWhiteSpace(_state.PendingTheme)) + { + Plugin.Config.Theme = _state.PendingTheme; + Plugin.ThemeRegistry.Switch(_state.PendingTheme); + } } private void ApplyPrivacyFirst() @@ -194,6 +480,20 @@ public sealed class FirstRunWizard : Window Plugin.Config.RetentionPerChannelDays = policy; } + private void ApplyRoleplay() + { + Plugin.Config.PrivacyFilterEnabled = true; + Plugin.Config.PrivacyPersistChannels = [.. PrivacyDefaults.RoleplayWhitelist]; + Plugin.Config.PrivacyPersistUnknownChannels = false; + + Plugin.Config.RetentionEnabled = true; + Plugin.Config.RetentionDefaultDays = 30; + var policy = PrivacyDefaults.DefaultRetentionDays.ToDictionary(p => p.Key, p => p.Value); + foreach (var (type, days) in PrivacyDefaults.RoleplayRetentionOverrides) + policy[type] = days; + Plugin.Config.RetentionPerChannelDays = policy; + } + private void ApplyFullHistory() { // Full history = upstream Chat 2 behavior. Filter off, retention off, @@ -205,4 +505,32 @@ public sealed class FirstRunWizard : Window Plugin.Config.RetentionEnabled = false; Plugin.Config.RetentionPerChannelDays.Clear(); } + + // Test-only entry point so SelfTests/WizardStateSmokeStep can advance + // the state machine without spawning ImGui input events. + internal void TestOnly_AdvanceTo(int step) => _state.CurrentStep = Math.Clamp(step, 1, TotalSteps); + + // Test-only setter so the smoke-test can pin a profile selection + // without driving the ImGui card-click path. + internal void TestOnly_SetPendingProfile(PrivacyProfile profile) => _state.PendingProfile = profile; + + internal enum PrivacyProfile + { + PrivacyFirst, + Casual, + Roleplay, + FullHistory, + } + + private sealed class WizardState + { + public int CurrentStep { get; set; } = 1; + public PrivacyProfile? PendingProfile { get; set; } + public bool? PendingLoadPreviousSession { get; set; } + public bool? PendingFilterIncludePreviousSessions { get; set; } + public int? PendingAutoTellTabsHistoryPreload { get; set; } + public bool? PendingUseCompactDensity { get; set; } + public bool? PendingPrettierTimestamps { get; set; } + public string? PendingTheme { get; set; } + } } From 1c820b7f53fa6f0e9adf84ae075c36f9d7c7ca5f Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Mon, 18 May 2026 22:03:50 +0200 Subject: [PATCH 04/10] test(selftest): register WizardStateSmokeStep for v1.5.2 wizard flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Variant 1 walks the FirstRunWizard state machine through Step 1 → 4 and commits with no pending values to verify the no-op write-back path. Variant 2 picks Roleplay on Step 2, skips Step 3, commits, and asserts LoadPreviousSession / FilterIncludePreviousSessions stayed on their pre-test value — pinning the null-semantics from Spec Z.176. ApplyRoleplay would overwrite six privacy / retention fields, so the step snapshots them before Variant 2 and CleanUp() restores them, keeping the self-test idempotent across /xlperf runs. Catches state-machine throws and CommitPending NREs that would otherwise surface as a hard plugin crash during Finish ✓ clicks. Runs alongside the existing three FontManager / ThemeSwitch self-test steps. --- HellionChat/Plugin.cs | 1 + HellionChat/SelfTests/WizardStateSmokeStep.cs | 133 ++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 HellionChat/SelfTests/WizardStateSmokeStep.cs diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index 74464d6..b486c8f 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -321,6 +321,7 @@ public sealed class Plugin : IAsyncDalamudPlugin new SelfTests.ThemeSwitchSelfTestStep(this), new SelfTests.FontManagerCtorSmokeStep(this), new SelfTests.FontPushSmokeStep(this), + new SelfTests.WizardStateSmokeStep(this), ]); if (!Config.FirstRunCompleted) diff --git a/HellionChat/SelfTests/WizardStateSmokeStep.cs b/HellionChat/SelfTests/WizardStateSmokeStep.cs new file mode 100644 index 0000000..3fc3d20 --- /dev/null +++ b/HellionChat/SelfTests/WizardStateSmokeStep.cs @@ -0,0 +1,133 @@ +using System.Collections.Generic; +using Dalamud.Bindings.ImGui; +using Dalamud.Plugin.SelfTest; +using HellionChat.Code; +using HellionChat.Ui; + +namespace HellionChat.SelfTests; + +// Drives the FirstRunWizard state machine through every step and +// commits a no-op pending state (Variant 1), then re-runs picking +// Roleplay on Step 2 and skipping Step 3 (Variant 2). Verifies +// that the staged-commit path does not throw under any combination +// of Pending* values and that CommitPending leaves Config in a +// readable shape. Variant 2's Roleplay commit would normally +// mutate the six PrivacyFilter / Retention fields ApplyRoleplay +// touches, so the step snapshots them before Variant 2 runs and +// CleanUp() restores them — the self-test stays idempotent across +// repeated /xlperf runs and does not overwrite an active privacy +// profile. +internal sealed class WizardStateSmokeStep : ISelfTestStep +{ + private readonly Plugin plugin; + + // Snapshot slots for the six Configuration fields ApplyRoleplay + // writes in Variant 2. Populated right before Variant 2 mutates + // Config, consumed by CleanUp(). Reference-typed snapshots + // (HashSet, Dictionary) capture the existing slot by reference, + // which is safe because ApplyRoleplay reassigns the slot with + // a fresh instance instead of mutating in place. + private bool? snapshotPrivacyFilterEnabled; + private HashSet? snapshotPrivacyPersistChannels; + private bool? snapshotPrivacyPersistUnknownChannels; + private bool? snapshotRetentionEnabled; + private int? snapshotRetentionDefaultDays; + private Dictionary? snapshotRetentionPerChannelDays; + + public WizardStateSmokeStep(Plugin plugin) + { + this.plugin = plugin; + } + + public string Name => "Hellion Chat - FirstRunWizard state smoke"; + + public SelfTestStepResult RunStep() + { + var wizard = this.plugin.FirstRunWizard; + if (wizard is null) + { + ImGui.Text("Plugin.FirstRunWizard is null"); + return SelfTestStepResult.Fail; + } + + try + { + // Variant 1: no-op CommitPending. Walks the state machine and + // verifies the empty-pending write-back path does not throw. + wizard.TestOnly_AdvanceTo(1); + wizard.TestOnly_AdvanceTo(2); + wizard.TestOnly_AdvanceTo(3); + wizard.TestOnly_AdvanceTo(4); + wizard.CommitPending(); + + // Variant 2: skip Step 3 explicitly. Picks Roleplay on Step 2, + // jumps straight to Step 4 (no Step-3 entry → no seed for + // LoadPreviousSession / FilterIncludePreviousSessions), commits, + // and asserts the two coupled history toggles remained on their + // pre-test value. Pins the null-semantics from Spec Z.176 so a + // regression in CommitPending that started writing seeded + // recommendations unconditionally would surface here. + // CommitPending → ApplyRoleplay overwrites six privacy / + // retention fields, so snapshot them first and let CleanUp + // restore them after the assert. Keeps /xlperf idempotent. + this.snapshotPrivacyFilterEnabled = Plugin.Config.PrivacyFilterEnabled; + this.snapshotPrivacyPersistChannels = Plugin.Config.PrivacyPersistChannels; + this.snapshotPrivacyPersistUnknownChannels = Plugin.Config.PrivacyPersistUnknownChannels; + this.snapshotRetentionEnabled = Plugin.Config.RetentionEnabled; + this.snapshotRetentionDefaultDays = Plugin.Config.RetentionDefaultDays; + this.snapshotRetentionPerChannelDays = Plugin.Config.RetentionPerChannelDays; + + var loadPrevBefore = Plugin.Config.LoadPreviousSession; + var filterPrevBefore = Plugin.Config.FilterIncludePreviousSessions; + wizard.TestOnly_AdvanceTo(2); + wizard.TestOnly_SetPendingProfile(FirstRunWizard.PrivacyProfile.Roleplay); + wizard.TestOnly_AdvanceTo(4); + wizard.CommitPending(); + if (Plugin.Config.LoadPreviousSession != loadPrevBefore) + { + ImGui.Text("Skip-Step-3 path overwrote LoadPreviousSession"); + return SelfTestStepResult.Fail; + } + if (Plugin.Config.FilterIncludePreviousSessions != filterPrevBefore) + { + ImGui.Text("Skip-Step-3 path overwrote FilterIncludePreviousSessions"); + return SelfTestStepResult.Fail; + } + } + catch (Exception ex) + { + ImGui.Text($"Wizard state smoke threw: {ex.GetType().Name}: {ex.Message}"); + return SelfTestStepResult.Fail; + } + + return SelfTestStepResult.Pass; + } + + public void CleanUp() + { + // Restore the six Variant-2 snapshots so back-to-back /xlperf + // runs don't drift the active privacy profile. If Variant 2 + // never ran (Variant 1 threw early), the slots stay null and + // restore is a no-op. After restore the slots are nulled so a + // future RunStep starts fresh. + if (this.snapshotPrivacyFilterEnabled is { } privacyFilter) + Plugin.Config.PrivacyFilterEnabled = privacyFilter; + if (this.snapshotPrivacyPersistChannels is { } persistChannels) + Plugin.Config.PrivacyPersistChannels = persistChannels; + if (this.snapshotPrivacyPersistUnknownChannels is { } persistUnknown) + Plugin.Config.PrivacyPersistUnknownChannels = persistUnknown; + if (this.snapshotRetentionEnabled is { } retentionEnabled) + Plugin.Config.RetentionEnabled = retentionEnabled; + if (this.snapshotRetentionDefaultDays is { } retentionDays) + Plugin.Config.RetentionDefaultDays = retentionDays; + if (this.snapshotRetentionPerChannelDays is { } retentionPolicy) + Plugin.Config.RetentionPerChannelDays = retentionPolicy; + + this.snapshotPrivacyFilterEnabled = null; + this.snapshotPrivacyPersistChannels = null; + this.snapshotPrivacyPersistUnknownChannels = null; + this.snapshotRetentionEnabled = null; + this.snapshotRetentionDefaultDays = null; + this.snapshotRetentionPerChannelDays = null; + } +} From 1e418ab86f078259af95d051d68497b3972dac29 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Mon, 18 May 2026 23:10:53 +0200 Subject: [PATCH 05/10] fix(ui): shrink wizard window and fold the Fox banner by default Smoke feedback v1.5.2 R1: the 900x560 default size dominated the screen and the centred MonoFont fox silhouette filled the welcome step. Default size drops to 720x480, MinimumSize to 600x400, so the wizard fits comfortably on a sub-monitor and still leaves the power-settings step readable when shrunk. Step 1 wraps the banner in a folded TreeNode (label "Hellion Forge", same anchor pattern the v1.5.1 wizard used) so the onboarding copy stays the primary focus and users opt into the silhouette explicitly. --- HellionChat/Ui/FirstRunWizard.cs | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/HellionChat/Ui/FirstRunWizard.cs b/HellionChat/Ui/FirstRunWizard.cs index 3fb46c5..8cf7e00 100644 --- a/HellionChat/Ui/FirstRunWizard.cs +++ b/HellionChat/Ui/FirstRunWizard.cs @@ -38,10 +38,10 @@ public sealed class FirstRunWizard : Window Flags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking; SizeCondition = ImGuiCond.Appearing; - Size = new Vector2(900, 560); + Size = new Vector2(720, 480); SizeConstraints = new WindowSizeConstraints { - MinimumSize = new Vector2(720, 480), + MinimumSize = new Vector2(600, 400), MaximumSize = new Vector2(float.MaxValue, float.MaxValue), }; } @@ -150,15 +150,24 @@ public sealed class FirstRunWizard : Window ImGui.TextUnformatted(HellionStrings.Wizard_Step1_Title); ImGui.Spacing(); - using (Plugin.Interface.UiBuilder.MonoFontHandle.Push()) + // Banner is opt-in: the full silhouette dominates the wizard window + // at the default size, so the TreeNode is folded by default and the + // onboarding copy stays the primary focus. Mirrors the pre-rewrite + // collapsible anchor from v1.5.1. + using (var tree = ImRaii.TreeNode("Hellion Forge")) { - // Centre the banner across the available region. Spec Z.96 - // calls for a zentrierten Mono-Font-Block. CalcTextSize must - // run inside the MonoFont push so the measurement matches the - // glyph width actually used for rendering. - var bannerSize = ImGui.CalcTextSize(HellionForgeAscii.FoxBanner); - ImGui.SetCursorPosX((ImGui.GetContentRegionAvail().X - bannerSize.X) * 0.5f); - ImGui.TextUnformatted(HellionForgeAscii.FoxBanner); + if (tree.Success) + { + using (Plugin.Interface.UiBuilder.MonoFontHandle.Push()) + { + // CalcTextSize must run inside the MonoFont push so the + // measurement matches the glyph width actually used for + // rendering. + var bannerSize = ImGui.CalcTextSize(HellionForgeAscii.FoxBanner); + ImGui.SetCursorPosX((ImGui.GetContentRegionAvail().X - bannerSize.X) * 0.5f); + ImGui.TextUnformatted(HellionForgeAscii.FoxBanner); + } + } } ImGui.Spacing(); From 9745abea0ce6846e599b5c123fe3c0429727a447 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Mon, 18 May 2026 23:18:19 +0200 Subject: [PATCH 06/10] feat(wizard): re-surface first-run wizard once for existing v1.5.2 users MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bestehende User haben FirstRunCompleted=true vom alten Single-Page Wizard und würden den neuen Multi-Step-Flow nie zu sehen bekommen. Neues Config-Feld WizardLastShownVersion (Default leer) trägt die Version, deren Wizard zuletzt gezeigt wurde. Plugin.LoadAsync vergleicht gegen die Konstante WizardReshowVersion ("1.5.2") und setzt FirstRunCompleted einmalig zurück, wenn die Werte abweichen. SaveConfig sofort danach, damit ein Pre-Finish-Crash die Re-Show nicht endlos wiederholt. Künftige Cycles bumpen die Konstante nur wenn der Wizard wirklich umstrukturiert wird. --- HellionChat/Configuration.cs | 10 ++++++++++ HellionChat/Plugin.cs | 13 +++++++++++++ 2 files changed, 23 insertions(+) diff --git a/HellionChat/Configuration.cs b/HellionChat/Configuration.cs index c7b0434..9d78591 100755 --- a/HellionChat/Configuration.cs +++ b/HellionChat/Configuration.cs @@ -100,6 +100,15 @@ public class Configuration : IPluginConfiguration public Dictionary RetentionPerChannelDays = []; public DateTimeOffset RetentionLastRunAt = DateTimeOffset.MinValue; public bool FirstRunCompleted; + + // Tracks which plugin version last surfaced the first-run wizard. + // When the running version is newer than this, Plugin.LoadAsync + // re-opens the wizard once so existing users see major UX reworks + // (e.g. the v1.5.2 multi-step rewrite). Skip path and Finish both + // set FirstRunCompleted = true on close, so the wizard only fires + // once per version bump even if the user dismisses it. + public string WizardLastShownVersion = string.Empty; + public bool UseHellionFont = true; public bool ShowHonorificTitleInHeader = true; @@ -336,6 +345,7 @@ public class Configuration : IPluginConfiguration RetentionLastRunAt = other.RetentionLastRunAt; FirstRunCompleted = other.FirstRunCompleted; + WizardLastShownVersion = other.WizardLastShownVersion; UseHellionFont = other.UseHellionFont; ShowHonorificTitleInHeader = other.ShowHonorificTitleInHeader; ShowHonorificGlow = other.ShowHonorificGlow; diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index b486c8f..6286521 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -324,6 +324,19 @@ public sealed class Plugin : IAsyncDalamudPlugin new SelfTests.WizardStateSmokeStep(this), ]); + // Re-surface the wizard for existing users when a major UX + // rework ships. The constant tracks the most recent version + // whose wizard should be shown once; bump it in future cycles + // that reshape the onboarding flow. Saved immediately so a + // pre-Finish crash doesn't loop the prompt forever. + const string WizardReshowVersion = "1.5.2"; + if (Config.WizardLastShownVersion != WizardReshowVersion) + { + Config.FirstRunCompleted = false; + Config.WizardLastShownVersion = WizardReshowVersion; + SaveConfig(); + } + if (!Config.FirstRunCompleted) FirstRunWizard.IsOpen = true; From e1f84a9b1038b418b34f0c17dcd5bf3832eee4fb Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Mon, 18 May 2026 23:29:56 +0200 Subject: [PATCH 07/10] chore(release): v1.5.2 manifest bump Bumps csproj Version, repo.json AssemblyVersion/TestingAssemblyVersion plus the three DownloadLink* URLs, yaml + repo.json changelog blocks (slim-rule: v1.5.2 + v1.5.1 + v1.5.0 + v1.4.10 retained, v1.4.9 trimmed to the Full history footer link), docs CHANGELOG long-form block, ROADMAP v1.5.2 marked complete and v1.5.3 set as next cycle (FR localisation with Hezcal native-speaker review), README status strings plus moved pre-v1.5.2 history. Changelog includes the in-cycle UI shrink + Fox-Banner-TreeNode smoke fix and the WizardLastShownVersion re-show-once mechanism for existing users. --- HellionChat/HellionChat.csproj | 2 +- HellionChat/HellionChat.yaml | 88 +++++++++++++++++++--------------- README.md | 24 +++++++--- docs/CHANGELOG.md | 47 ++++++++++++++++++ docs/ROADMAP.md | 33 ++++++++++--- repo.json | 14 +++--- 6 files changed, 149 insertions(+), 59 deletions(-) diff --git a/HellionChat/HellionChat.csproj b/HellionChat/HellionChat.csproj index d3cba89..b03c0c0 100644 --- a/HellionChat/HellionChat.csproj +++ b/HellionChat/HellionChat.csproj @@ -1,7 +1,7 @@ - 1.5.1 + 1.5.2 enable enable diff --git a/HellionChat/HellionChat.yaml b/HellionChat/HellionChat.yaml index 29f356b..926450b 100755 --- a/HellionChat/HellionChat.yaml +++ b/HellionChat/HellionChat.yaml @@ -35,6 +35,56 @@ tags: - Replacement - Privacy changelog: |- + **v1.5.2 — First-Run Wizard Rework (2026-05-18)** + + UX patch. The first-run wizard becomes a four-step flow with a + new Roleplay privacy profile and a power-settings step that + surfaces previously-hidden defaults. Existing v1.5.1 users see + the new wizard once on first v1.5.2 boot. + + What changes user-visible: + + - Wizard navigation: Welcome → Privacy profile → Power settings + → Done. Forge-Bronze pagination dots, dedicated stage for the + power settings so they are no longer buried in Settings. + - Fourth privacy profile "Roleplay": Privacy-First plus Say and + both emote types, with a 30-day window for Say and a 90-day + window for emotes. Shout, Yell and Novice Network stay out. + - Privacy picker becomes a 2x2 grid. Casual stays the + recommended option with a ★ marker. + - Power-settings step covers Load Previous Session, Filter + Include Previous Sessions, Auto-Tell-Tabs History Preload, + Compact Density, Prettier Timestamps and a built-in theme + picker. All six map to existing Configuration fields — no new + settings introduced. + - Staged commit: the wizard only writes to Config on the Finish + step. Decide-later or X-close at any point leaves the existing + config untouched. + - Inline test hint on the done step: "type /tell + into chat" surfaces the auto-tell-tab spawn mechanism. + - Window starts at 720x480 (was 900x560) and can shrink to + 600x400; Step 1 keeps the fox banner in a folded TreeNode so + the onboarding copy stays primary. + - Existing users get the new wizard surfaced once on first boot + after the update via the new WizardLastShownVersion config + field. Future cycles bump the constant only when the wizard + itself changes shape. + + Under the hood: + + - WizardStateSmokeStep added to /xlperf alongside the FontManager + and ThemeSwitch self-tests. + - Twelve new pure-helper xUnit Facts in the Build Suite cover + all four privacy profile sets and their retention overrides. + + Migration v17 stays (no schema bump). The Configuration grows + one optional string field (WizardLastShownVersion) which + defaults to empty for legacy users. + + Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2). + + --- + **v1.5.1 — FontAtlas Refactor and Hellion Forge Signature (2026-05-17)** Hybrid FontManager refactor plus an embedded provenance mark. @@ -177,42 +227,4 @@ changelog: |- --- - **v1.4.9 — Plugin-Load Render Polish (2026-05-15)** - - Tenth sub-patch of the v1.4.x polish-sweep series. First-frame - render cost drops from ~127 ms median to ~76 ms median, - comfortably under Dalamud's 100 ms HITCH warning threshold. - - - First-frame defer: six non-essential rendering sections inside - ChatLogWindow skip their first Draw and run one frame later - (bottom status bar, channel-name SeString chunks, window bounds - check, v0.6.1 hint banner, autocomplete, input-preview - calculation). User-visible delay is ~17 ms at 60 fps, hidden - inside the post-reload font-atlas build window. - - Slash-command centralisation: /hellion, /hellionView, - /hellionSeString and /hellionDebugger are registered in - LoadAsync instead of inside the corresponding window - constructors. The plugin-manager Open and configuration buttons - hang on the same path. - - Plugin-load profiling logs stay on at Information level - (MessageStore connect/migrate, FilterAllTabs, auto-translate - warmup) as a regression tripwire — a future load past 100 ms - will show up in /xllog without a Debug filter. - - ChatTwo IPC compatibility layer: HellionChat now mirrors - ChatTwo's full IPC surface (GetChatInputState, - ChatInputStateChanged, Register, Unregister, Available, - Invoke) under the ChatTwo.* namespace in addition to our - existing HellionChat.* provider gates. Third-party - integrations that historically only subscribe to ChatTwo's - IPC — for example Artisan's and AllaganTools' context-menu - hooks — keep working without requiring a code change on their - side. Conflict detection prevents ChatTwo from loading in - parallel with HellionChat, so there is no slot-collision risk - at runtime. - - Migration v17 stays (no schema bump). - - Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2). - - --- - Full history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases diff --git a/README.md b/README.md index fbd9f71..67190f8 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.1-brightgreen)](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/latest) +[![Latest release](https://img.shields.io/badge/release-v1.5.2-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,7 +11,7 @@ Hellion Forge

-**Version 1.5.1** — Privacy-first chat plugin for FINAL FANTASY XIV / Dalamud, built on +**Version 1.5.2** — Privacy-first chat plugin for FINAL FANTASY XIV / Dalamud, built on [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 @@ -299,6 +299,22 @@ An optional submission to the Dalamud main plugin repo (in addition to the custo ## Project Status +**Version 1.5.2** — First-Run Wizard Rework. The single-page wizard becomes a four-step +staged-commit flow (Welcome → Privacy → Power Settings → Done). The privacy picker becomes a 2×2 +grid with a fourth profile "Roleplay" that extends Privacy-First with `Say` and both emote types +under a 30-/90-day retention window. A power-settings stage surfaces six previously-hidden +`Configuration` defaults in one place without introducing any new settings. The wizard window +shrinks to 720×480 default (was 900×560, MinimumSize 600×400) after smoke feedback and Step 1 +keeps the fox banner in a folded TreeNode so the onboarding copy stays primary. Existing v1.5.1 +users see the new flow once on first v1.5.2 boot via a new `WizardLastShownVersion` config marker. +Under the hood: a `WizardStateSmokeStep` joins `/xlperf`, the Build Suite gains twelve pure-helper +xUnit Facts pinning all four privacy profile sets and the new Roleplay retention overrides. +Migration v17 stays — `Configuration` only grows one optional string field. + +--- + +### Project status (pre-v1.5.2, kept for context) + **Version 1.5.1** — FontAtlas Refactor and Hellion Forge Signature. The FontManager moves from the inherited Chat 2 anti-pattern (null! fields + a separate BuildFonts method) to a hybrid model where the game fonts and FontAwesome are init-only handles and only the user-configurable delegate fonts @@ -318,10 +334,6 @@ defer their font-atlas build to land at ~7 ms; Chat 2 + HellionChat were ~75 ms) cost lives in the UiBuilder first-frame render path, not in the atlas build. A first-frame render investigation is reserved for a later cycle. ---- - -### Project status (pre-v1.5.1, kept for context) - **Version 1.5.0** — DI Foundation and Service Refactor. Major architecture cycle: the plugin bootstrap moves to a generic-host DI container (`Microsoft.Extensions.Hosting` + `IServiceCollection`) modelled on Lightless Sync. All 18 instance-class services migrate from a diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 5a3c273..9c7f38b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -11,6 +11,53 @@ releases as an overview and links to the release pages for details. --- +## Hellion Chat 1.5.2 — First-Run Wizard Rework (2026-05-18) + +UX patch. The single-page first-run wizard becomes a four-step staged-commit flow, the privacy +profile catalogue gains a fourth entry "Roleplay", and a new power-settings stage surfaces six +previously-hidden Configuration defaults. Existing v1.5.1 users see the new wizard once on first +v1.5.2 boot via a new `WizardLastShownVersion` config marker. + +User-visible: + +- Wizard layout: Welcome → Privacy profile → Power settings → Done. Forge-Bronze pagination dots, + per-step Back / Decide later / Next footer. Decide-later and X-close both leave the existing + config untouched; only the Finish ✓ click commits pending choices. +- Fourth privacy profile "Roleplay": Privacy-First whitelist plus `Say` and both emote types, with a + 30-day retention window for `Say` and 90 days for the two emote channels. `Shout`, `Yell` and + `NoviceNetwork` stay out — public-distance noise from strangers is not story content. +- Privacy picker becomes a 2×2 grid. Casual stays the recommended option with a ★ marker. +- Power-settings stage surfaces six existing `Configuration` fields in one place: Load Previous + Session, Filter Include Previous Sessions, Auto-Tell-Tabs History Preload, Compact Density, + Prettier Timestamps, plus a built-in theme picker. No new settings are introduced — the stage just + collects what was previously buried in Settings → Privacy / Chat / Data Management / Optik. +- Inline test hint on the done stage: `type /tell into chat` surfaces the auto-tell-tab + spawn mechanism for new users. +- Wizard window starts at 720×480 (was 900×560) and can shrink to 600×400. Step 1 wraps the fox + banner in a collapsible TreeNode, folded by default — onboarding copy stays primary. +- Existing v1.5.1 users get the new wizard surfaced once on first v1.5.2 boot. A new + `WizardLastShownVersion` config field tracks the most recent version whose wizard was shown; + Plugin.LoadAsync resets `FirstRunCompleted` once when the constant `1.5.2` doesn't match. + +Under the hood: + +- `WizardStateSmokeStep` registered with `/xlperf`. Variant 1 walks the four steps with empty + pending state to pin the no-op CommitPending path. Variant 2 picks Roleplay on Step 2, skips + Step 3, commits, and asserts `LoadPreviousSession` / `FilterIncludePreviousSessions` stayed on + their pre-test value — pinning the null-semantics contract. The step snapshots six privacy / + retention fields before Variant 2 and `CleanUp()` restores them, so back-to-back runs don't drift + the active profile. +- Twelve pure-helper xUnit Facts in the Build Suite (`Privacy/PrivacyDefaultsTests.cs`) cover all + four profile whitelists plus the new Roleplay retention overrides. +- `Configuration` grows one optional string field `WizardLastShownVersion` (default empty). No + schema bump — migration v17 still applies. + +Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2). EUPL-1.2. + +[Full release notes on the Gitea release page.](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/v1.5.2) + +--- + ## Hellion Chat 1.5.1 — FontAtlas Refactor and Hellion Forge Signature (2026-05-17) Hybrid FontManager refactor plus an embedded Hellion Forge provenance mark. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 4e5cc37..23cb108 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -10,14 +10,33 @@ be a poor fit for the plugin's privacy-first scope during brainstorming. --- -## Next Cycle (v1.5.2) +## Next Cycle (v1.5.3) -**First-Run-Wizard rework with curated defaults beyond the three privacy profiles.** Jin's discovery -in v1.4.10 surfaced the wizard's three-card layout as too thin — power users want richer presets out -of the box. After that, FR localisation (Hezcal native-speaker review confirmed), then the Plugin -Integrations Wave 2-6 (Context-Menu, NotificationMaster, Moodles, ExtraChat, XIVIM Quick-DM). The -UiBuilder first-frame HITCH investigation that v1.5.1 surfaced sits as a separate spike near the -Wine/Linux scroll-rubber-band investigation at the tail. +**French localisation.** Strings from `Resources/HellionStrings.resx` get a FR translation pass +(DeepL first draft), then Hezcal native-speaker review before release. After that, the Plugin +Integrations Wave 2-6 (Context-Menu, NotificationMaster, Moodles, ExtraChat, XIVIM Quick-DM) and the +UiBuilder first-frame HITCH investigation that v1.5.1 surfaced are queued behind it, alongside the +Wine/Linux scroll-rubber-band spike at the tail. + +--- + +## v1.5.2 — First-Run Wizard Rework (released 2026-05-18) + +Multi-step wizard replacement: Welcome → Privacy → Power Settings → Done with staged-commit so +Decide-later or X-close at any point leaves the existing config untouched. New fourth privacy +profile "Roleplay" extends Privacy-First with `Say` and both emote types under a 30-/90-day +retention window. Privacy picker becomes a 2×2 grid; Casual keeps the ★ recommended marker. A new +power-settings stage surfaces six previously-hidden `Configuration` fields (Load Previous Session, +Filter Include Previous Sessions, Auto-Tell-Tabs History Preload, Compact Density, Prettier +Timestamps, built-in theme picker) without introducing any new fields. + +Window default size shrinks from 900×560 to 720×480 (MinimumSize 600×400) and Step 1 wraps the fox +banner in a folded TreeNode after smoke feedback. Existing v1.5.1 users see the new wizard once on +first v1.5.2 boot via a new `WizardLastShownVersion` config marker. + +Under the hood: `WizardStateSmokeStep` joins the `/xlperf` lineup, the Build Suite gains twelve +pure-helper xUnit Facts pinning all four privacy profile sets and the new Roleplay retention +overrides. Migration v17 stays — `Configuration` only grows one optional string field. --- diff --git a/repo.json b/repo.json index 287821d..34cdb1d 100644 --- a/repo.json +++ b/repo.json @@ -3,8 +3,8 @@ "Author": "Jon Kazama (Hellion Forge)", "Name": "Hellion Chat", "InternalName": "HellionChat", - "AssemblyVersion": "1.5.1.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 three profiles: Privacy-First, Casual, Full History\n- Bilingual UI (EN/DE) 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", + "AssemblyVersion": "1.5.2.0", + "Description": "A Hellion Forge plugin \u2014 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 three profiles: Privacy-First, Casual, Full History\n- Bilingual UI (EN/DE) with live language switching\n- Own config and database \u2014 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"], @@ -14,12 +14,12 @@ "CanUnloadAsync": false, "LoadPriority": 0, "Punchline": "A Hellion Forge plugin. Privacy-first chat for FFXIV, built to stay out of your way.", - "Changelog": "**v1.5.1 \u2014 FontAtlas Refactor and Hellion Forge Signature (2026-05-17)**\n\nHybrid FontManager refactor plus an embedded provenance mark.\n\nWhat changes under the hood:\n\n- FontManager handle creation moves into the ctor inside a single\n atlas.SuppressAutoRebuild() block. The font atlas now builds\n once per plugin load instead of four to five times \u2014 less CPU\n and GPU pressure in the first seconds after a reload, less\n atlas texture memory churn.\n- Hybrid property model: Axis, AxisItalic and FontAwesome become\n init-only handles. RegularFont and ItalicFont stay mutable\n because the eight font settings still need to replace them at\n runtime \u2014 that path is funnelled through RebuildDelegateFonts()\n now and runs without a plugin reload.\n- FontAwesome reuses Dalamud's UiBuilder.IconFontFixedWidthHandle\n instead of building its own atlas slot. One delegate-build\n step less in the ctor.\n- BuildFontsAsync and BuildFonts are removed; the live mutation\n path is RebuildDelegateFonts() now.\n- Two FontManager self-test steps registered with /xlperf: ctor\n smoke (every handle non-null after Phase-1 resolve, no atlas\n load-exception) and push smoke (Push() returns without throwing).\n\nHonorific full-gradient port (originally the v1.5.1 main item)\nwas dropped: Honorific 3.2 exposes no IPC for the rendered\ngradient frame, and an in-plugin port of the colour palette was\ndeclined. The integration stays at the v1.4.7 glow-only shape.\n\nUser-visible:\n\n- Hellion Forge signature: a small fox-head ASCII silhouette is\n emitted to /xllog on every plugin load, and a full fox banner\n with \"Hellion Forge\" set inside the body is available as a\n folded TreeNode in the First-Run Wizard and Settings ->\n Information tab. Drawn by Julia Moon, embedded in the plugin DLL.\n- No settings changes, no migration. v17 stays.\n\nNote on performance: the cross-plugin baseline target from\nv1.5.0 (matching Lightless and XIVInstantMessenger at ~7 ms\nHITCH) did not land this cycle. HITCH stays around 80 ms because\nthe cost is in the UiBuilder first-frame render path, not in the\natlas build (which this cycle did reduce from 4-5 builds per\nload to 1). A first-frame render investigation is reserved for\na later cycle.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\n**v1.5.0 \u2014 DI Foundation and Service Refactor (2026-05-17)**\n\nMajor architecture cycle. The plugin bootstrap moves to a\ngeneric-host DI container (Microsoft.Extensions.Hosting +\nIServiceCollection) modelled on Lightless Sync. Service logging\nmoves from a static Plugin.LogProxy locator to typed\nMicrosoft.Extensions.Logging.ILogger via constructor injection,\nbridged over Dalamud's IPluginLog by a custom DalamudLogger trio.\n\nWhat changes under the hood:\n\n- 18 instance-class services migrate to ILogger via constructor\n injection across four slices: data layer (MessageStore,\n MessageManager, AutoTellTabsService), IPC and integrations\n (HonorificService, IpcManager, TypingIpc, ExtraChat, the three\n GameFunctions classes), UI window layer (ChatLogWindow,\n DbViewer, Popout, three settings tabs), and root (Commands,\n ThemeRegistry, PayloadHandler).\n- Plugin.LogProxy stays in place for the eight buckets ctor\n injection cannot reach: static helpers (EmoteCache,\n AutoTranslate, MemoryUtil, WrapperUtil), Dalamud-reflected\n types (Configuration), the Message data class, and instance\n classes that only log from static methods (FontManager, one\n GameFunctions site).\n- Plugin.cs finishes at 1012 lines \u2014 virtually identical to the\n pre-cycle 1013. The new Phase-1 host build and Plugin.X bridge\n wiring trade out exactly the service and window allocations\n that previously lived in LoadAsync.\n- Cross-plugin baseline confirms no performance penalty against\n Chat 2: HellionChat first-frame HITCH 77 ms median, Chat 2\n 74 ms median. Lightless and XIVInstantMessenger sit around\n 7 ms by deferring their font-atlas build past Finished\n loading \u2014 that pattern is the v1.5.1 follow-up.\n\nUser-visible:\n\n- Slash-command insert fix: pasting a slash command into the\n chat input (Friend List \"/tell\" action, plugin-driven inserts\n from Artisan, AllaganTools etc.) now replaces the existing\n input instead of concatenating. Cherry-picked from ChatTwo\n upstream ee7768ac with namespace adaptation.\n\nMigration v17 stays (no schema bump).\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\n**v1.4.10 \u2014 Symbol-Picker and Tell-History Fix (2026-05-16)**\n\nEleventh and final sub-patch of the v1.4.x polish-sweep series.\nSymbol picker for the chat input, a tell-history reload fix for\nusers with many active partners, and a closing cleanup sweep\nbefore v1.5.0 picks up the DI-container adoption.\n\n- Symbol picker: a small smile-icon button left of the channel\n indicator opens a popup with two tabs. The first lists all 161\n FFXIV PUA glyphs (Dalamud's SeIconChar enum); the second\n carries 97 server-verified BMP symbols (latin marks, currency,\n the full Greek alphabet, geometric shapes, suits, notes) \u2014\n every one of them round-tripped through /echo and /say in a\n four-round probe so the in-channel render matches what the\n picker shows. Click drops the glyph at the caret, multi-insert\n keeps the popup open, and a recent-used strip floats the last\n sixteen picks across both tabs. Toggle in Settings \u2192 Chat \u2192\n Message behaviour, default on.\n- Pinned auto-tell tabs reload their full history again: a\n hidden 500-row scan cap in PreloadHistory used to override the\n user-configurable AutoTellTabsHistoryPreload setting, so\n less-frequent pinned partners (rare /tell sessions in an\n otherwise busy week) lost their backlog. The cap is removed;\n the (Receiver, Date) index keeps SQL fast, the client-side\n loop still respects your setting as the upper bound.\n- Slash-command teardown: /hellion, /hellionView,\n /hellionDebugger (and #if DEBUG /hellionSeString) wrappers are\n now cached as private fields. Plugin teardown detaches the\n live registration instead of re-Register'ing with identical\n args \u2014 closes a latent maintenance hazard from v1.4.9.\n- v1.4.x polish-sweep wraps up here. The ImGuiListClipper render\n refactor that was on the v1.4.10 reserve list got dropped\n after cross-platform smoke showed the scroll rubber-band is a\n Wine / Linux render-pipeline quirk, not universal \u2014 Windows\n users never saw it. It will get its own platform-targeted\n spike in a later patch. Next major cycle is v1.5.0 with the\n DI-container adoption (Microsoft.Extensions.Hosting +\n ILogger) modelled on Lightless.\n- Migration v17 stays (no schema bump).\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\n**v1.4.9 \u2014 Plugin-Load Render Polish (2026-05-15)**\n\nTenth sub-patch of the v1.4.x polish-sweep series. First-frame\nrender cost drops from ~127 ms median to ~76 ms median,\ncomfortably under Dalamud's 100 ms HITCH warning threshold.\n\n- First-frame defer: six non-essential rendering sections inside\n ChatLogWindow skip their first Draw and run one frame later\n (bottom status bar, channel-name SeString chunks, window bounds\n check, v0.6.1 hint banner, autocomplete, input-preview\n calculation). User-visible delay is ~17 ms at 60 fps, hidden\n inside the post-reload font-atlas build window.\n- Slash-command centralisation: /hellion, /hellionView,\n /hellionSeString and /hellionDebugger are registered in\n LoadAsync instead of inside the corresponding window\n constructors. The plugin-manager Open and configuration buttons\n hang on the same path.\n- Plugin-load profiling logs stay on at Information level\n (MessageStore connect/migrate, FilterAllTabs, auto-translate\n warmup) as a regression tripwire \u2014 a future load past 100 ms\n will show up in /xllog without a Debug filter.\n- ChatTwo IPC compatibility layer: HellionChat now mirrors\n ChatTwo's full IPC surface (GetChatInputState,\n ChatInputStateChanged, Register, Unregister, Available,\n Invoke) under the ChatTwo.* namespace in addition to our\n existing HellionChat.* provider gates. Third-party\n integrations that historically only subscribe to ChatTwo's\n IPC \u2014 for example Artisan's and AllaganTools' context-menu\n hooks \u2014 keep working without requiring a code change on their\n side. Conflict detection prevents ChatTwo from loading in\n parallel with HellionChat, so there is no slot-collision risk\n at runtime.\n- Migration v17 stays (no schema bump).\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", + "Changelog": "**v1.5.2 \u2014 First-Run Wizard Rework (2026-05-18)**\n\nUX patch. The first-run wizard becomes a four-step flow with a\nnew Roleplay privacy profile and a power-settings step that\nsurfaces previously-hidden defaults. Existing v1.5.1 users see\nthe new wizard once on first v1.5.2 boot.\n\nWhat changes user-visible:\n\n- Wizard navigation: Welcome \u2192 Privacy profile \u2192 Power settings\n \u2192 Done. Forge-Bronze pagination dots, dedicated stage for the\n power settings so they are no longer buried in Settings.\n- Fourth privacy profile \"Roleplay\": Privacy-First plus Say and\n both emote types, with a 30-day window for Say and a 90-day\n window for emotes. Shout, Yell and Novice Network stay out.\n- Privacy picker becomes a 2x2 grid. Casual stays the\n recommended option with a \u2605 marker.\n- Power-settings step covers Load Previous Session, Filter\n Include Previous Sessions, Auto-Tell-Tabs History Preload,\n Compact Density, Prettier Timestamps and a built-in theme\n picker. All six map to existing Configuration fields \u2014 no new\n settings introduced.\n- Staged commit: the wizard only writes to Config on the Finish\n step. Decide-later or X-close at any point leaves the existing\n config untouched.\n- Inline test hint on the done step: \"type /tell \n into chat\" surfaces the auto-tell-tab spawn mechanism.\n- Window starts at 720x480 (was 900x560) and can shrink to\n 600x400; Step 1 keeps the fox banner in a folded TreeNode so\n the onboarding copy stays primary.\n- Existing users get the new wizard surfaced once on first boot\n after the update via the new WizardLastShownVersion config\n field. Future cycles bump the constant only when the wizard\n itself changes shape.\n\nUnder the hood:\n\n- WizardStateSmokeStep added to /xlperf alongside the FontManager\n and ThemeSwitch self-tests.\n- Twelve new pure-helper xUnit Facts in the Build Suite cover\n all four privacy profile sets and their retention overrides.\n\nMigration v17 stays (no schema bump). The Configuration grows\none optional string field (WizardLastShownVersion) which\ndefaults to empty for legacy users.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\n**v1.5.1 \u2014 FontAtlas Refactor and Hellion Forge Signature (2026-05-17)**\n\nHybrid FontManager refactor plus an embedded provenance mark.\n\nWhat changes under the hood:\n\n- FontManager handle creation moves into the ctor inside a single\n atlas.SuppressAutoRebuild() block. The font atlas now builds once\n per plugin load instead of four to five times \u2014 less CPU and GPU\n pressure in the first seconds after a reload, less atlas texture\n memory churn.\n- Hybrid property model: Axis, AxisItalic and FontAwesome become\n init-only handles. RegularFont and ItalicFont stay mutable because\n the eight font settings still need to replace them at runtime \u2014\n that path is funnelled through RebuildDelegateFonts() now and\n runs without a plugin reload.\n- FontAwesome reuses Dalamud's UiBuilder.IconFontFixedWidthHandle\n instead of building its own atlas slot. One delegate-build step\n less in the ctor.\n- BuildFontsAsync and BuildFonts are removed; the live mutation\n path is RebuildDelegateFonts() now.\n- Two FontManager self-test steps registered with /xlperf: ctor\n smoke (every handle non-null after Phase-1 resolve, no atlas\n load-exception) and push smoke (Push() returns without throwing).\n\nHonorific full-gradient port (originally the v1.5.1 main item) was\ndropped: Honorific 3.2 exposes no IPC for the rendered gradient\nframe, and an in-plugin port of the colour palette was declined.\nThe integration stays at the v1.4.7 glow-only shape.\n\nUser-visible:\n\n- Hellion Forge signature: a small fox-head ASCII silhouette is\n emitted to /xllog on every plugin load, and a full fox banner\n with \"Hellion Forge\" set inside the body is available as a\n folded TreeNode in the First-Run Wizard and Settings ->\n Information tab. Drawn by Julia Moon, embedded in the plugin DLL.\n- No settings changes, no migration. v17 stays.\n\nNote on performance: the cross-plugin baseline target from v1.5.0\n(matching Lightless and XIVInstantMessenger at ~7 ms HITCH) did\nnot land this cycle. HITCH stays around 80 ms because the cost is\nin the UiBuilder first-frame render path, not in the atlas build\n(which this cycle did reduce from 4-5 builds per load to 1). A\nfirst-frame render investigation is reserved for a later cycle.\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\n**v1.5.0 \u2014 DI Foundation and Service Refactor (2026-05-17)**\n\nMajor architecture cycle. The plugin bootstrap moves to a\ngeneric-host DI container (Microsoft.Extensions.Hosting +\nIServiceCollection) modelled on Lightless Sync. Service logging\nmoves from a static Plugin.LogProxy locator to typed\nMicrosoft.Extensions.Logging.ILogger via constructor injection,\nbridged over Dalamud's IPluginLog by a custom DalamudLogger trio.\n\nWhat changes under the hood:\n\n- 18 instance-class services migrate to ILogger via constructor\n injection across four slices: data layer (MessageStore,\n MessageManager, AutoTellTabsService), IPC and integrations\n (HonorificService, IpcManager, TypingIpc, ExtraChat, the three\n GameFunctions classes), UI window layer (ChatLogWindow,\n DbViewer, Popout, three settings tabs), and root (Commands,\n ThemeRegistry, PayloadHandler).\n- Plugin.LogProxy stays in place for the eight buckets ctor\n injection cannot reach: static helpers (EmoteCache,\n AutoTranslate, MemoryUtil, WrapperUtil), Dalamud-reflected\n types (Configuration), the Message data class, and instance\n classes that only log from static methods (FontManager, one\n GameFunctions site).\n- Plugin.cs finishes at 1012 lines \u2014 virtually identical to the\n pre-cycle 1013. The new Phase-1 host build and Plugin.X bridge\n wiring trade out exactly the service and window allocations\n that previously lived in LoadAsync.\n- Cross-plugin baseline confirms no performance penalty against\n Chat 2: HellionChat first-frame HITCH 77 ms median, Chat 2\n 74 ms median. Lightless and XIVInstantMessenger sit around\n 7 ms by deferring their font-atlas build past Finished\n loading \u2014 that pattern is the v1.5.1 follow-up.\n\nUser-visible:\n\n- Slash-command insert fix: pasting a slash command into the\n chat input (Friend List \"/tell\" action, plugin-driven inserts\n from Artisan, AllaganTools etc.) now replaces the existing\n input instead of concatenating. Cherry-picked from ChatTwo\n upstream ee7768ac with namespace adaptation.\n\nMigration v17 stays (no schema bump).\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\n**v1.4.10 \u2014 Symbol-Picker and Tell-History Fix (2026-05-16)**\n\nEleventh and final sub-patch of the v1.4.x polish-sweep series.\nSymbol picker for the chat input, a tell-history reload fix for\nusers with many active partners, and a closing cleanup sweep\nbefore v1.5.0 picks up the DI-container adoption.\n\n- Symbol picker: a small smile-icon button left of the channel\n indicator opens a popup with two tabs. The first lists all 161\n FFXIV PUA glyphs (Dalamud's SeIconChar enum); the second\n carries 97 server-verified BMP symbols (latin marks, currency,\n the full Greek alphabet, geometric shapes, suits, notes) \u2014\n every one of them round-tripped through /echo and /say in a\n four-round probe so the in-channel render matches what the\n picker shows. Click drops the glyph at the caret, multi-insert\n keeps the popup open, and a recent-used strip floats the last\n sixteen picks across both tabs. Toggle in Settings \u2192 Chat \u2192\n Message behaviour, default on.\n- Pinned auto-tell tabs reload their full history again: a\n hidden 500-row scan cap in PreloadHistory used to override the\n user-configurable AutoTellTabsHistoryPreload setting, so\n less-frequent pinned partners (rare /tell sessions in an\n otherwise busy week) lost their backlog. The cap is removed;\n the (Receiver, Date) index keeps SQL fast, the client-side\n loop still respects your setting as the upper bound.\n- Slash-command teardown: /hellion, /hellionView,\n /hellionDebugger (and #if DEBUG /hellionSeString) wrappers are\n now cached as private fields. Plugin teardown detaches the\n live registration instead of re-Register'ing with identical\n args \u2014 closes a latent maintenance hazard from v1.4.9.\n- v1.4.x polish-sweep wraps up here. The ImGuiListClipper render\n refactor that was on the v1.4.10 reserve list got dropped\n after cross-platform smoke showed the scroll rubber-band is a\n Wine / Linux render-pipeline quirk, not universal \u2014 Windows\n users never saw it. It will get its own platform-targeted\n spike in a later patch. Next major cycle is v1.5.0 with the\n DI-container adoption (Microsoft.Extensions.Hosting +\n ILogger) modelled on Lightless.\n- Migration v17 stays (no schema bump).\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.1/latest.zip", - "DownloadLinkUpdate": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.5.1/latest.zip", - "DownloadLinkTesting": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.5.1/latest.zip", - "TestingAssemblyVersion": "1.5.1.0", + "DownloadLinkInstall": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.5.2/latest.zip", + "DownloadLinkUpdate": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.5.2/latest.zip", + "DownloadLinkTesting": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.5.2/latest.zip", + "TestingAssemblyVersion": "1.5.2.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", From 003bd5c695c812816d1fa3dd8e0e48b9bc05a7c9 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Mon, 18 May 2026 23:36:13 +0200 Subject: [PATCH 08/10] docs(changelog): polish v1.5.2 prose hygiene Fixes two minor copy-paste artefacts in the v1.5.2 CHANGELOG block: the duplicate trailing "EUPL-1.2." right after the Based-on footer, and a stray German "Optik" tab name in the power-settings list (the settings tab is "Appearance" in EN, the German label only appears in the localised UI). Yaml / repo.json / ROADMAP / README already used the right wording. --- docs/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 9c7f38b..8d21dc4 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -30,7 +30,7 @@ User-visible: - Power-settings stage surfaces six existing `Configuration` fields in one place: Load Previous Session, Filter Include Previous Sessions, Auto-Tell-Tabs History Preload, Compact Density, Prettier Timestamps, plus a built-in theme picker. No new settings are introduced — the stage just - collects what was previously buried in Settings → Privacy / Chat / Data Management / Optik. + collects what was previously buried in Settings → Privacy / Chat / Data Management / Appearance. - Inline test hint on the done stage: `type /tell into chat` surfaces the auto-tell-tab spawn mechanism for new users. - Wizard window starts at 720×480 (was 900×560) and can shrink to 600×400. Step 1 wraps the fox @@ -52,7 +52,7 @@ Under the hood: - `Configuration` grows one optional string field `WizardLastShownVersion` (default empty). No schema bump — migration v17 still applies. -Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2). EUPL-1.2. +Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2). [Full release notes on the Gitea release page.](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/v1.5.2) From 271a6ae6508186207fb9f215e786751cebeb2350 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Mon, 18 May 2026 23:42:44 +0200 Subject: [PATCH 09/10] docs(forge): add v1.5.2 forge announcement post body MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bilingual layout: DE in this file, EN extracted by forge-announce.yml from HellionChat.yaml changelog block. Body covers the four-step wizard rewrite, the new Roleplay profile, the surfaced power settings, the staged-commit + test-hint pattern, the WizardLastShownVersion re-show-once mechanism for existing users and the under-the-hood test additions. Subtitle 54 chars, versionsnatur 8 chars, embed sum (forge body + en-yaml + footer) 4158 chars — all under the workflow caps (60 / 40 / 5500). --- .github/forge-posts/v1.5.2.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .github/forge-posts/v1.5.2.md diff --git a/.github/forge-posts/v1.5.2.md b/.github/forge-posts/v1.5.2.md new file mode 100644 index 0000000..b0c9c17 --- /dev/null +++ b/.github/forge-posts/v1.5.2.md @@ -0,0 +1,10 @@ +--- +subtitle: "First-Run Wizard — neu in 4 Steps, Roleplay-Profil neu" +versionsnatur: "UX-Patch" +--- +- **Vier Steps statt Single-Page.** Der First-Run-Wizard öffnet jetzt in vier Bühnen: Willkommen → Privacy-Profil → Power-Settings → Fertig. Pagination-Dots in Forge-Bronze oben rechts, Back/Skip/Next im Footer. Standardgröße 720×480 (Min 600×400) und der Fuchs-Banner sitzt als zugeklappter TreeNode oben in Step 1, damit die Einleitung im Fokus bleibt. +- **Neues Privacy-Profil „Roleplay".** Datensparsamkeit plus Sagen und beide Emote-Typen für Story-Logs. Schreien und Rufen bleiben außen vor, Public-Distance-Lärm von Fremden ist kein Story-Inhalt. Aufbewahrung: Sagen 30 Tage, Emotes 90 Tage. Privacy-Picker wird zum 2×2-Grid, Casual bleibt mit ★-Marker als Empfehlung. +- **Power-Settings sichtbar.** Bislang versteckte Defaults bekommen eine eigene Bühne: Vorherige Session laden, Filter inkl. alter Messages, N Tell-Messages vorladen, Compact-Density, Prettier-Timestamps und Theme-Picker für die 10 Built-in-Themes. Keine neuen Settings, nur das Bestehende sauber sichtbar. +- **Staged-Commit und Test-Hint auf der Fertig-Bühne.** Auswahl wird erst beim Klick auf „Fertig ✓" geschrieben. „Später entscheiden" oder X-Close lässt die bestehende Config unangetastet, ein nicht angefasster Step behält die alten Werte. Direkt darunter sichtbar: „Tipp /tell ", plus die aktuelle Preload-Zahl aus Step 3 als Hinweis auf den Auto-Tell-Tab-Spawn. +- **Bestehende User sehen den neuen Wizard einmal.** Wer schon v1.5.1 hatte, bekommt den Multi-Step-Flow beim ersten v1.5.2-Boot aufgepoppt. Neues Config-Feld `WizardLastShownVersion` triggert das einmalig pro Wizard-Rework; Skip oder Finish reicht und danach öffnet er nicht mehr automatisch. +- **Unter der Haube.** Pure-Helper-Tests für alle vier Profile-Sets in der Build-Suite (zwölf neue Facts), plus ein WizardStateSmokeStep für `/xlperf`. Migration v17 bleibt, nur ein optionales Config-Feld kommt dazu. From 35efdd4628b2527200d48592bc4b28c4232d296c Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Mon, 18 May 2026 23:46:00 +0200 Subject: [PATCH 10/10] style(wizard): reflow FirstRunWizard and WizardStateSmokeStep to csharpier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Preflight Block E (`dotnet csharpier check`) flagged two reflows in the v1.5.2 code: the ForgeBronzeDim Vector4 constant needed multi-line form, and a handful of switch arms / long Plugin.Config chains in WizardStateSmokeStep needed line-breaks at csharpier's print-width. Pure formatting — zero functional change. Block D build stays clean, Block E now passes. --- HellionChat/SelfTests/WizardStateSmokeStep.cs | 4 +- HellionChat/Ui/FirstRunWizard.cs | 243 +++++++++++++----- 2 files changed, 188 insertions(+), 59 deletions(-) diff --git a/HellionChat/SelfTests/WizardStateSmokeStep.cs b/HellionChat/SelfTests/WizardStateSmokeStep.cs index 3fc3d20..f815314 100644 --- a/HellionChat/SelfTests/WizardStateSmokeStep.cs +++ b/HellionChat/SelfTests/WizardStateSmokeStep.cs @@ -72,7 +72,9 @@ internal sealed class WizardStateSmokeStep : ISelfTestStep // restore them after the assert. Keeps /xlperf idempotent. this.snapshotPrivacyFilterEnabled = Plugin.Config.PrivacyFilterEnabled; this.snapshotPrivacyPersistChannels = Plugin.Config.PrivacyPersistChannels; - this.snapshotPrivacyPersistUnknownChannels = Plugin.Config.PrivacyPersistUnknownChannels; + this.snapshotPrivacyPersistUnknownChannels = Plugin + .Config + .PrivacyPersistUnknownChannels; this.snapshotRetentionEnabled = Plugin.Config.RetentionEnabled; this.snapshotRetentionDefaultDays = Plugin.Config.RetentionDefaultDays; this.snapshotRetentionPerChannelDays = Plugin.Config.RetentionPerChannelDays; diff --git a/HellionChat/Ui/FirstRunWizard.cs b/HellionChat/Ui/FirstRunWizard.cs index 8cf7e00..c991fc3 100644 --- a/HellionChat/Ui/FirstRunWizard.cs +++ b/HellionChat/Ui/FirstRunWizard.cs @@ -24,7 +24,12 @@ public sealed class FirstRunWizard : Window // 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 static readonly Vector4 ForgeBronzeDim = new( + 0xC2 / 255f, + 0x41 / 255f, + 0x0C / 255f, + 0.3f + ); private const int TotalSteps = 4; @@ -64,11 +69,22 @@ public sealed class FirstRunWizard : Window switch (_state.CurrentStep) { - case 1: DrawStepWelcome(); break; - case 2: DrawStepPrivacy(); break; - case 3: DrawStepPowerSettings(); break; - case 4: DrawStepDone(); break; - default: _state.CurrentStep = 1; DrawStepWelcome(); break; + case 1: + DrawStepWelcome(); + break; + case 2: + DrawStepPrivacy(); + break; + case 3: + DrawStepPowerSettings(); + break; + case 4: + DrawStepDone(); + break; + default: + _state.CurrentStep = 1; + DrawStepWelcome(); + break; } } @@ -86,7 +102,11 @@ public sealed class FirstRunWizard : Window { 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); + draw.AddCircleFilled( + new Vector2(startX + i * spacing, cursor.Y + radius), + radius, + packed + ); } // Reserve vertical space the circles consumed so the next widget starts below them. @@ -96,7 +116,8 @@ public sealed class FirstRunWizard : Window private void DrawFooter(bool showBack, bool showSkip, string primaryLabel, Action onPrimary) { var spacing = ImGui.GetStyle().ItemSpacing.Y; - var primaryWidth = ImGui.CalcTextSize(primaryLabel).X + ImGui.GetStyle().FramePadding.X * 2 + 16f; + var primaryWidth = + ImGui.CalcTextSize(primaryLabel).X + ImGui.GetStyle().FramePadding.X * 2 + 16f; var avail = ImGui.GetContentRegionAvail(); // Push the footer to the bottom of the window so step contents @@ -175,8 +196,12 @@ public sealed class FirstRunWizard : Window ImGui.Spacing(); ImGui.TextWrapped(HellionStrings.Wizard_Step1_Footer_Hint); - DrawFooter(showBack: false, showSkip: true, HellionStrings.Wizard_Nav_Next, - () => _state.CurrentStep = 2); + DrawFooter( + showBack: false, + showSkip: true, + HellionStrings.Wizard_Nav_Next, + () => _state.CurrentStep = 2 + ); } private void DrawStepPrivacy() @@ -186,51 +211,92 @@ public sealed class FirstRunWizard : Window // 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 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; // Top row. - DrawProfileCard(PrivacyProfile.PrivacyFirst, "🔒", + DrawProfileCard( + PrivacyProfile.PrivacyFirst, + "🔒", HellionStrings.Wizard_Profile_PrivacyFirst_Heading, HellionStrings.Wizard_Profile_PrivacyFirst_Description, - recommended: false, cardWidth, cardHeight); + recommended: false, + cardWidth, + cardHeight + ); ImGui.SameLine(); - DrawProfileCard(PrivacyProfile.Casual, "💬", + DrawProfileCard( + PrivacyProfile.Casual, + "💬", HellionStrings.Wizard_Profile_Casual_Heading, HellionStrings.Wizard_Profile_Casual_Description, - recommended: true, cardWidth, cardHeight); + recommended: true, + cardWidth, + cardHeight + ); // Bottom row. - DrawProfileCard(PrivacyProfile.Roleplay, "🎭", + DrawProfileCard( + PrivacyProfile.Roleplay, + "🎭", HellionStrings.Wizard_Profile_Roleplay_Heading, HellionStrings.Wizard_Profile_Roleplay_Description, - recommended: false, cardWidth, cardHeight); + recommended: false, + cardWidth, + cardHeight + ); ImGui.SameLine(); - DrawProfileCard(PrivacyProfile.FullHistory, "📚", + DrawProfileCard( + PrivacyProfile.FullHistory, + "📚", HellionStrings.Wizard_Profile_FullHistory_Heading, HellionStrings.Wizard_Profile_FullHistory_Description, - recommended: false, cardWidth, cardHeight); + recommended: false, + cardWidth, + cardHeight + ); ImGui.Spacing(); ImGui.TextDisabled(HellionStrings.Wizard_Step2_RecommendedFooter); - DrawFooter(showBack: true, showSkip: true, HellionStrings.Wizard_Nav_Next, - () => _state.CurrentStep = 3); + DrawFooter( + showBack: true, + showSkip: true, + HellionStrings.Wizard_Nav_Next, + () => _state.CurrentStep = 3 + ); } - private void DrawProfileCard(PrivacyProfile profile, string emoji, string heading, string description, bool recommended, float width, float height) + private void DrawProfileCard( + PrivacyProfile profile, + string emoji, + string heading, + string description, + bool recommended, + 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 borderColor = isSelected + ? ImGui.GetColorU32(ForgeBronze) + : ImGui.GetColorU32(ImGuiCol.Border); using var _border = ImRaii.PushColor(ImGuiCol.Border, borderColor); - using var child = ImRaii.Child($"##profile-card-{profile}", new Vector2(width, height), true); + using var child = ImRaii.Child( + $"##profile-card-{profile}", + new Vector2(width, height), + true + ); if (!child.Success) return; @@ -290,7 +356,12 @@ public sealed class FirstRunWizard : Window } var filterPrev = _state.PendingFilterIncludePreviousSessions ?? true; - if (ImGui.Checkbox(HellionStrings.Wizard_Step3_FilterIncludePreviousSessions_Label, ref filterPrev)) + if ( + ImGui.Checkbox( + HellionStrings.Wizard_Step3_FilterIncludePreviousSessions_Label, + ref filterPrev + ) + ) { _state.PendingFilterIncludePreviousSessions = filterPrev; if (!filterPrev) @@ -303,8 +374,16 @@ public sealed class FirstRunWizard : Window 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)) + var preload = + _state.PendingAutoTellTabsHistoryPreload ?? Plugin.Config.AutoTellTabsHistoryPreload; + if ( + ImGui.SliderInt( + HellionStrings.Wizard_Step3_AutoTellTabsHistoryPreload_Label, + ref preload, + 0, + 100 + ) + ) _state.PendingAutoTellTabsHistoryPreload = preload; ImGui.Spacing(); @@ -325,11 +404,18 @@ public sealed class FirstRunWizard : Window // 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)); + 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)) + using ( + var combo = ImRaii.Combo( + HellionStrings.Wizard_Step3_Theme_Label, + builtIns[currentIndex].Name + ) + ) { if (combo.Success) { @@ -344,8 +430,12 @@ public sealed class FirstRunWizard : Window } } - DrawFooter(showBack: true, showSkip: true, HellionStrings.Wizard_Nav_Next, - () => _state.CurrentStep = 4); + DrawFooter( + showBack: true, + showSkip: true, + HellionStrings.Wizard_Nav_Next, + () => _state.CurrentStep = 4 + ); } private void DrawStepDone() @@ -378,51 +468,74 @@ public sealed class FirstRunWizard : Window var profileLabel = _state.PendingProfile switch { - PrivacyProfile.PrivacyFirst => HellionStrings.Wizard_Profile_PrivacyFirst_Heading, + PrivacyProfile.PrivacyFirst => + HellionStrings.Wizard_Profile_PrivacyFirst_Heading, PrivacyProfile.Casual => HellionStrings.Wizard_Profile_Casual_Heading, PrivacyProfile.Roleplay => HellionStrings.Wizard_Profile_Roleplay_Heading, PrivacyProfile.FullHistory => HellionStrings.Wizard_Profile_FullHistory_Heading, _ => HellionStrings.Wizard_Step4_Summary_Unchanged, }; - ImGui.TextWrapped(string.Format(HellionStrings.Wizard_Step4_Summary_Profile, profileLabel)); + ImGui.TextWrapped( + string.Format(HellionStrings.Wizard_Step4_Summary_Profile, profileLabel) + ); - var historyLabel = (_state.PendingLoadPreviousSession ?? false) - ? HellionStrings.Wizard_Step3_LoadPreviousSession_Label - : HellionStrings.Wizard_Step4_Summary_Unchanged; - ImGui.TextWrapped(string.Format(HellionStrings.Wizard_Step4_Summary_History, historyLabel)); + var historyLabel = + (_state.PendingLoadPreviousSession ?? false) + ? HellionStrings.Wizard_Step3_LoadPreviousSession_Label + : HellionStrings.Wizard_Step4_Summary_Unchanged; + ImGui.TextWrapped( + string.Format(HellionStrings.Wizard_Step4_Summary_History, historyLabel) + ); - var preloadValue = _state.PendingAutoTellTabsHistoryPreload ?? Plugin.Config.AutoTellTabsHistoryPreload; - ImGui.TextWrapped(string.Format(HellionStrings.Wizard_Step4_Summary_TellTabs, preloadValue)); + var preloadValue = + _state.PendingAutoTellTabsHistoryPreload + ?? Plugin.Config.AutoTellTabsHistoryPreload; + ImGui.TextWrapped( + string.Format(HellionStrings.Wizard_Step4_Summary_TellTabs, preloadValue) + ); var compact = _state.PendingUseCompactDensity ?? Plugin.Config.UseCompactDensity; var pretty = _state.PendingPrettierTimestamps ?? Plugin.Config.PrettierTimestamps; var themeSlug = _state.PendingTheme ?? Plugin.Config.Theme; var themeName = Plugin.ThemeRegistry.Get(themeSlug).Name; var visualParts = new List(); - if (compact) visualParts.Add(HellionStrings.Wizard_Step3_UseCompactDensity_Label); - if (pretty) visualParts.Add(HellionStrings.Wizard_Step3_PrettierTimestamps_Label); + if (compact) + visualParts.Add(HellionStrings.Wizard_Step3_UseCompactDensity_Label); + if (pretty) + visualParts.Add(HellionStrings.Wizard_Step3_PrettierTimestamps_Label); visualParts.Add(themeName); - ImGui.TextWrapped(string.Format(HellionStrings.Wizard_Step4_Summary_Visual, string.Join(", ", visualParts))); + ImGui.TextWrapped( + string.Format( + HellionStrings.Wizard_Step4_Summary_Visual, + string.Join(", ", visualParts) + ) + ); } } ImGui.Spacing(); // Inline FR-3 hint with placeholder for preload count. - var preloadForHint = _state.PendingAutoTellTabsHistoryPreload ?? Plugin.Config.AutoTellTabsHistoryPreload; + var preloadForHint = + _state.PendingAutoTellTabsHistoryPreload ?? Plugin.Config.AutoTellTabsHistoryPreload; using (ImRaii.PushColor(ImGuiCol.Text, ForgeBronze)) ImGui.TextWrapped(string.Format(HellionStrings.Wizard_Step4_TestHint, preloadForHint)); ImGui.Spacing(); ImGui.TextDisabled(HellionStrings.Wizard_Step4_SettingsHint); - DrawFooter(showBack: true, showSkip: false, HellionStrings.Wizard_Nav_Finish, () => - { - CommitPending(); - Plugin.Config.FirstRunCompleted = true; - Plugin.SaveConfig(); - IsOpen = false; - }); + DrawFooter( + showBack: true, + showSkip: false, + HellionStrings.Wizard_Nav_Finish, + () => + { + CommitPending(); + Plugin.Config.FirstRunCompleted = true; + Plugin.SaveConfig(); + IsOpen = false; + } + ); } // Writes only non-null pending values back to Config. A null pending @@ -433,20 +546,32 @@ public sealed class FirstRunWizard : Window { switch (_state.PendingProfile) { - case PrivacyProfile.PrivacyFirst: ApplyPrivacyFirst(); break; - case PrivacyProfile.Casual: ApplyCasual(); break; - case PrivacyProfile.Roleplay: ApplyRoleplay(); break; - case PrivacyProfile.FullHistory: ApplyFullHistory(); break; + case PrivacyProfile.PrivacyFirst: + ApplyPrivacyFirst(); + break; + case PrivacyProfile.Casual: + ApplyCasual(); + break; + case PrivacyProfile.Roleplay: + ApplyRoleplay(); + break; + case PrivacyProfile.FullHistory: + ApplyFullHistory(); + break; } if (_state.PendingLoadPreviousSession.HasValue) Plugin.Config.LoadPreviousSession = _state.PendingLoadPreviousSession.Value; if (_state.PendingFilterIncludePreviousSessions.HasValue) - Plugin.Config.FilterIncludePreviousSessions = _state.PendingFilterIncludePreviousSessions.Value; + Plugin.Config.FilterIncludePreviousSessions = _state + .PendingFilterIncludePreviousSessions + .Value; if (_state.PendingAutoTellTabsHistoryPreload.HasValue) - Plugin.Config.AutoTellTabsHistoryPreload = _state.PendingAutoTellTabsHistoryPreload.Value; + Plugin.Config.AutoTellTabsHistoryPreload = _state + .PendingAutoTellTabsHistoryPreload + .Value; if (_state.PendingUseCompactDensity.HasValue) Plugin.Config.UseCompactDensity = _state.PendingUseCompactDensity.Value; @@ -517,11 +642,13 @@ public sealed class FirstRunWizard : Window // Test-only entry point so SelfTests/WizardStateSmokeStep can advance // the state machine without spawning ImGui input events. - internal void TestOnly_AdvanceTo(int step) => _state.CurrentStep = Math.Clamp(step, 1, TotalSteps); + internal void TestOnly_AdvanceTo(int step) => + _state.CurrentStep = Math.Clamp(step, 1, TotalSteps); // Test-only setter so the smoke-test can pin a profile selection // without driving the ImGui card-click path. - internal void TestOnly_SetPendingProfile(PrivacyProfile profile) => _state.PendingProfile = profile; + internal void TestOnly_SetPendingProfile(PrivacyProfile profile) => + _state.PendingProfile = profile; internal enum PrivacyProfile {