diff --git a/HellionChat/Configuration.cs b/HellionChat/Configuration.cs index c1291c5..94427ac 100755 --- a/HellionChat/Configuration.cs +++ b/HellionChat/Configuration.cs @@ -35,7 +35,7 @@ public class ConfigKeyBind [Serializable] public class Configuration : IPluginConfiguration { - internal const int LatestVersion = 24; + internal const int LatestVersion = 25; public int Version { get; set; } = LatestVersion; @@ -184,8 +184,6 @@ public class Configuration : IPluginConfiguration // v1.2.1: default flipped false → true for consistency with other hide defaults. public bool HideInNewGamePlusMenu = true; public bool HideWhenInactive; - public int InactivityHideTimeout = 10; - public bool InactivityHideActiveDuringBattle = true; [Obsolete("Use InactivityHideChannelsV2 instead")] public Dictionary InactivityHideChannels = []; @@ -243,7 +241,6 @@ public class Configuration : IPluginConfiguration // UI-11: warn before sending a message that carries plugin-only glyphs. public bool NotifyPluginDisclosure = true; public bool KeepInputFocus = true; - public int MaxLinesToRender = 2_500; // 1-10000 public bool Use24HourClock = true; public bool ShowEmotes = true; public HashSet BlockedEmotes = []; @@ -310,164 +307,6 @@ public class Configuration : IPluginConfiguration // v22 field: MainWindow layout mode (sidebar vs. horizontal top tabs). // Initializer doubles as the migration default for configs loaded at v21. public MainWindowLayoutMode MainWindowLayoutMode = MainWindowLayoutMode.Sidebar; - - public void UpdateFrom(Configuration other, bool backToOriginal) - { - if (backToOriginal) - { - // NOTE (v1.8.0): this only flips the PopOut flag back. If a future - // caller ever wires UpdateFrom(backToOriginal: true) to a live - // settings-cancel path, that CALL-SITE must also iterate - // ChannelPopoutPool.TryClose over the affected Tab.Identifiers, - // otherwise pool windows stay IsOpen=true while the flag is false - // (orphan window). The pool is not reachable from this POCO by design. - foreach (var tab in Tabs.Where(t => t.PopOut)) - tab.PopOut = false; - } - - HideChat = other.HideChat; - HideDuringCutscenes = other.HideDuringCutscenes; - HideWhenNotLoggedIn = other.HideWhenNotLoggedIn; - HideWhenUiHidden = other.HideWhenUiHidden; - HideInLoadingScreens = other.HideInLoadingScreens; - HideInBattle = other.HideInBattle; - HideInNewGamePlusMenu = other.HideInNewGamePlusMenu; - HideWhenInactive = other.HideWhenInactive; - InactivityHideTimeout = other.InactivityHideTimeout; - InactivityHideActiveDuringBattle = other.InactivityHideActiveDuringBattle; - InactivityHideChannelsV2 = other.InactivityHideChannelsV2.ToDictionary( - pair => pair.Key, - pair => pair.Value - ); - InactivityHideExtraChatAll = other.InactivityHideExtraChatAll; - InactivityHideExtraChatChannels = other.InactivityHideExtraChatChannels.ToHashSet(); - ShowHideButton = other.ShowHideButton; - NativeItemTooltips = other.NativeItemTooltips; - ScreenshotMode = other.ScreenshotMode; - PrettierTimestamps = other.PrettierTimestamps; - MoreCompactPretty = other.MoreCompactPretty; - HideSameTimestamps = other.HideSameTimestamps; - ShowNoviceNetwork = other.ShowNoviceNetwork; - SidebarTabView = other.SidebarTabView; - PrintChangelog = other.PrintChangelog; - OnlyPreviewIf = other.OnlyPreviewIf; - PreviewMinimum = other.PreviewMinimum; - PreviewPosition = other.PreviewPosition; - CommandHelpSide = other.CommandHelpSide; - KeybindMode = other.KeybindMode; - LanguageOverride = other.LanguageOverride; - CanMove = other.CanMove; - CanResize = other.CanResize; - ShowTitleBar = other.ShowTitleBar; - ShowPopOutTitleBar = other.ShowPopOutTitleBar; - DatabaseBattleMessages = other.DatabaseBattleMessages; - FilterIncludePreviousSessions = other.FilterIncludePreviousSessions; - SortAutoTranslate = other.SortAutoTranslate; - CollapseDuplicateMessages = other.CollapseDuplicateMessages; - CollapseKeepUniqueLinks = other.CollapseKeepUniqueLinks; - SymbolPickerEnabled = other.SymbolPickerEnabled; - PlaySounds = other.PlaySounds; - CustomSoundVolume = other.CustomSoundVolume; - NotifyFailedTell = other.NotifyFailedTell; - NotifyPluginDisclosure = other.NotifyPluginDisclosure; - KeepInputFocus = other.KeepInputFocus; - MaxLinesToRender = other.MaxLinesToRender; - Use24HourClock = other.Use24HourClock; - ShowEmotes = other.ShowEmotes; - // Deep-copy so settings window edits don't leak into live config before Save. - BlockedEmotes = new HashSet(other.BlockedEmotes); - FontsEnabled = other.FontsEnabled; - ItalicEnabled = other.ItalicEnabled; - ExtraGlyphRanges = other.ExtraGlyphRanges; - FontSizeV2 = other.FontSizeV2; - GlobalFontV2 = other.GlobalFontV2; - JapaneseFontV2 = other.JapaneseFontV2; - ItalicFontV2 = other.ItalicFontV2; - SymbolsFontSizeV2 = other.SymbolsFontSizeV2; - TooltipOffset = other.TooltipOffset; - ChatColours = other.ChatColours.ToDictionary(entry => entry.Key, entry => entry.Value); - ColorSelectedInputChannelButton = other.ColorSelectedInputChannelButton; - - // Keep live temp tabs alive across UpdateFrom — a settings save must - // not destroy open tell conversations. Pinned TempTabs are persistent - // and come through `other` like regular tabs; unpinned TempTabs are - // session-only and held from the local state. For persistent tabs - // (incl. pinned), capture live runtime state by Identifier and restore - // it onto the freshly cloned tabs — CurrentChannel is critical because - // the user may have switched channel in-game between settings-open - // and settings-save, and we'd otherwise overwrite that with the - // settings-time snapshot. - var liveUnpinnedTempTabs = Tabs.Where(TabLifecycleHelpers.IsInUnpinnedPool).ToList(); - var livePersistentSession = Tabs.Where(t => !TabLifecycleHelpers.IsInUnpinnedPool(t)) - .ToDictionary(t => t.Identifier, t => (t.Messages, t.LastSendUnread, t.CurrentChannel)); - - Tabs = other - .Tabs.Where(t => !t.IsTempTab || t.IsPinned) - .Select(t => - { - var clone = t.Clone(); - if (livePersistentSession.TryGetValue(clone.Identifier, out var live)) - { - clone.Messages = live.Messages; - clone.LastSendUnread = live.LastSendUnread; - clone.CurrentChannel = live.CurrentChannel; - } - return clone; - }) - .ToList(); - Tabs.AddRange(liveUnpinnedTempTabs); - - ChatTabForward = other.ChatTabForward; - ChatTabBackward = other.ChatTabBackward; - - PrivacyFilterEnabled = other.PrivacyFilterEnabled; - PrivacyPersistChannels = [.. other.PrivacyPersistChannels]; - PrivacyPersistUnknownChannels = other.PrivacyPersistUnknownChannels; - - RetentionEnabled = other.RetentionEnabled; - RetentionDefaultDays = other.RetentionDefaultDays; - RetentionPerChannelDays = other.RetentionPerChannelDays.ToDictionary( - p => p.Key, - p => p.Value - ); - RetentionLastRunAt = other.RetentionLastRunAt; - - FirstRunCompleted = other.FirstRunCompleted; - WizardLastShownVersion = other.WizardLastShownVersion; - UseHellionFont = other.UseHellionFont; - ShowHonorificTitleInHeader = other.ShowHonorificTitleInHeader; - ShowHonorificGlow = other.ShowHonorificGlow; - - // v1.1.0 theme engine fields - Theme = other.Theme; - WindowOpacity = other.WindowOpacity; - WindowOpacityInactive = other.WindowOpacityInactive; - ReduceMotion = other.ReduceMotion; - UseCompactDensity = other.UseCompactDensity; - - EnableAutoTellTabs = other.EnableAutoTellTabs; - AutoTellTabsLimit = other.AutoTellTabsLimit; - AutoTellTabsCompactDisplay = other.AutoTellTabsCompactDisplay; - AutoTellTabsHistoryPreload = other.AutoTellTabsHistoryPreload; - SidebarWidth = other.SidebarWidth; - AutoTellTabsShowGreetedToggle = other.AutoTellTabsShowGreetedToggle; - - SeenPopOutInputHint = other.SeenPopOutInputHint; - PopOutInputEnabled = other.PopOutInputEnabled; - SeenPopOutHeaderHint = other.SeenPopOutHeaderHint; - AutoTellTabsOpenAsPopout = other.AutoTellTabsOpenAsPopout; - - WorldSuffixMode = other.WorldSuffixMode; - NameFormMode = other.NameFormMode; - - MainWindowOpen = other.MainWindowOpen; - SettingsWindowOpen = other.SettingsWindowOpen; - MaxParallelPopouts = other.MaxParallelPopouts; - TellAutoOpenMode = other.TellAutoOpenMode; - TellAutoOpenSwitchAlways = other.TellAutoOpenSwitchAlways; - SidebarAutoSwitchThresholdPx = other.SidebarAutoSwitchThresholdPx; - MainWindowLayoutMode = other.MainWindowLayoutMode; - } } [Serializable] @@ -540,9 +379,6 @@ public class Tab // Optional FontAwesome glyph name; null falls back to TabIconMapping default. public string? Icon = null; - [Obsolete("Removed in favor of SelectedChannels")] - public Dictionary ChatCodes = new(); - public Dictionary SelectedChannels = new(); public bool ExtraChatAll; public HashSet ExtraChatChannels = []; @@ -559,12 +395,15 @@ public class Tab public bool CanMove = true; public bool CanResize = true; - public bool IndependentHide; - public bool HideDuringCutscenes = true; - public bool HideWhenNotLoggedIn = true; - public bool HideWhenUiHidden = true; - public bool HideInLoadingScreens; - public bool HideInBattle; + // Six per-tab hide conditions used to live here. Their reader was the + // pop-out window, which stopped consulting them in cf4705e; the equivalents + // that survive are the window-level fields of the same name further up this + // file, and v1.12.0 gave every one of those a control. + // + // Per-tab was the wrong unit anyway: "hide during cutscenes" is a statement + // about the screen, not about one conversation. + // + // HideWhenInactive stays -- the auto-tell service writes it. public bool HideWhenInactive; public bool IsTempTab; @@ -667,15 +506,14 @@ public class Tab public Tab Clone() { -#pragma warning disable CS0618 // ChatCodes is obsolete but still serialized and must survive a clone return new Tab { Name = Name, - // Both were missing: Icon feeds the sidebar glyph, ChatCodes carries - // legacy migration data that is still written to the JSON. A clone - // round-trip used to drop them silently. + // Icon feeds the sidebar glyph and a clone round-trip used to drop + // it silently. ChatCodes sat beside it until v1.12.0, carrying data + // for a migration that the v16 schema gate had already made + // unreachable. Icon = Icon, - ChatCodes = new Dictionary(ChatCodes), SelectedChannels = SelectedChannels.ToDictionary(pair => pair.Key, pair => pair.Value), ExtraChatAll = ExtraChatAll, ExtraChatChannels = ExtraChatChannels.ToHashSet(), @@ -693,12 +531,6 @@ public class Tab CurrentChannel = CurrentChannel.Clone(), CanMove = CanMove, CanResize = CanResize, - IndependentHide = IndependentHide, - HideDuringCutscenes = HideDuringCutscenes, - HideWhenNotLoggedIn = HideWhenNotLoggedIn, - HideWhenUiHidden = HideWhenUiHidden, - HideInLoadingScreens = HideInLoadingScreens, - HideInBattle = HideInBattle, HideWhenInactive = HideWhenInactive, IsTempTab = IsTempTab, IsPinned = IsPinned, @@ -708,7 +540,6 @@ public class Tab NotificationSoundId = NotificationSoundId, IsGreeted = IsGreeted, }; -#pragma warning restore CS0618 } /// Ordered message list with duplicate ID tracking, sorting and mutex protection. diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index a80bcbd..9e28c46 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -167,6 +167,14 @@ public sealed class Plugin : IAsyncDalamudPlugin // via Framework.RunOnTick (v1.4.8 B3 retention sweep) can early-bail // before they touch state that has already been torn down. Volatile // because the tick reads it from a different thread than the writer. + // The three hide conditions v1.5.6 evaluated and cf4705e left without a + // reader. Advanced once per draw, before any window is drawn. + private Util.ChatHideReason _hideReason = Util.ChatHideReason.None; + + // Set by the chat-activation keybind, consumed by the next hide evaluation. + // A cutscene the user dismissed stays dismissed until it ends. + internal bool ChatActivationRequested; + private volatile bool _isDisposing; // Read by background workers that outlive a teardown -- the export thread @@ -309,7 +317,13 @@ public sealed class Plugin : IAsyncDalamudPlugin ); } - Config.Version = 24; + // v25 carries no migration step. The schema gate above only refuses + // anything under 16, and Json.NET drops keys it does not recognise on + // load, so the fields v1.12.0 deleted simply stop being written on the + // next save. The bump is documentation, and it has to be consistent: + // the constant and this stamp are two separate places, and changing + // only one gives a config that re-stamps itself on every start. + Config.Version = 25; // Unpinned TempTabs are session-only and dropped on every load. Pinned // TempTabs survive reload — Jin's tester feedback (v1.4.7). @@ -470,7 +484,7 @@ public sealed class Plugin : IAsyncDalamudPlugin new SelfTests.SettingsWindowOpenStep(this), new SelfTests.OnOpenMainUiRoutesMainWindowStep(this), new SelfTests.TypingIpcStateStep(this), - new SelfTests.ConfigMigrationV24Step(this), + new SelfTests.ConfigMigrationV25Step(this), new SelfTests.ChannelPopoutBindStep(this), new SelfTests.HoverStateFootprintStep(), new SelfTests.HonorificHeaderRenderStep(this), @@ -1203,6 +1217,32 @@ public sealed class Plugin : IAsyncDalamudPlugin Interface.UiBuilder.DisableUserUiHide = !Config.HideWhenUiHidden; + // Stateless, so it needs no machine: there is no gesture that shows + // the chat while nobody is logged in. + if (Config.HideWhenNotLoggedIn && !ClientState.IsLoggedIn) + { + TypingIpc.Update(); + return; + } + + _hideReason = Util.ChatHideState.Next( + _hideReason, + new Util.ChatHideState.Inputs( + Config.HideInBattle, + InBattle, + Config.HideDuringCutscenes, + CutsceneActive || GposeActive, + ChatActivationRequested + ) + ); + ChatActivationRequested = false; + + if (Util.ChatHideState.Hides(_hideReason)) + { + TypingIpc.Update(); + return; + } + // RegularFont is nullable only because the live rebuild path // disposes it before reassigning; both ends of that swap happen on // this same draw thread, so it cannot be null here. diff --git a/HellionChat/Resources/HellionStrings.Designer.cs b/HellionChat/Resources/HellionStrings.Designer.cs index 7f57fef..c4b4a68 100644 --- a/HellionChat/Resources/HellionStrings.Designer.cs +++ b/HellionChat/Resources/HellionStrings.Designer.cs @@ -147,6 +147,7 @@ internal class HellionStrings 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_Summary_Off => Get(nameof(Wizard_Step4_Summary_Off)); internal static string Wizard_Step4_TestHint => Get(nameof(Wizard_Step4_TestHint)); internal static string Wizard_Step4_SettingsHint => Get(nameof(Wizard_Step4_SettingsHint)); diff --git a/HellionChat/Resources/HellionStrings.ca.resx b/HellionChat/Resources/HellionStrings.ca.resx index c238ed5..dfbc04c 100644 --- a/HellionChat/Resources/HellionStrings.ca.resx +++ b/HellionChat/Resources/HellionStrings.ca.resx @@ -1214,4 +1214,7 @@ Insereix l'objecte enllaçat <item> + + desactivat + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.cs.resx b/HellionChat/Resources/HellionStrings.cs.resx index dca569f..4508fe2 100644 --- a/HellionChat/Resources/HellionStrings.cs.resx +++ b/HellionChat/Resources/HellionStrings.cs.resx @@ -1213,4 +1213,7 @@ Vložit odkázaný předmět <item> + + vypnuto + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.da.resx b/HellionChat/Resources/HellionStrings.da.resx index af39218..5ca8ba7 100644 --- a/HellionChat/Resources/HellionStrings.da.resx +++ b/HellionChat/Resources/HellionStrings.da.resx @@ -1213,4 +1213,7 @@ Indsæt linket genstand <item> + + fra + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.de.resx b/HellionChat/Resources/HellionStrings.de.resx index d86d3fa..2f657d5 100644 --- a/HellionChat/Resources/HellionStrings.de.resx +++ b/HellionChat/Resources/HellionStrings.de.resx @@ -1208,4 +1208,7 @@ Verlinkten Gegenstand einfügen <item> + + aus + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.el.resx b/HellionChat/Resources/HellionStrings.el.resx index d0d44c0..8b0c41c 100644 --- a/HellionChat/Resources/HellionStrings.el.resx +++ b/HellionChat/Resources/HellionStrings.el.resx @@ -1213,4 +1213,7 @@ Εισαγωγή συνδεδεμένου αντικειμένου <item> + + ανενεργό + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.es.resx b/HellionChat/Resources/HellionStrings.es.resx index b550565..d3b323d 100644 --- a/HellionChat/Resources/HellionStrings.es.resx +++ b/HellionChat/Resources/HellionStrings.es.resx @@ -1214,4 +1214,7 @@ Insertar objeto enlazado <item> + + desactivado + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.fi.resx b/HellionChat/Resources/HellionStrings.fi.resx index 4527a49..0d88537 100644 --- a/HellionChat/Resources/HellionStrings.fi.resx +++ b/HellionChat/Resources/HellionStrings.fi.resx @@ -1213,4 +1213,7 @@ Lisää linkitetty esine <item> + + pois + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.fr.resx b/HellionChat/Resources/HellionStrings.fr.resx index 3385228..ec917d2 100644 --- a/HellionChat/Resources/HellionStrings.fr.resx +++ b/HellionChat/Resources/HellionStrings.fr.resx @@ -1214,4 +1214,7 @@ Insérer l'objet lié <item> + + désactivé + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.hu.resx b/HellionChat/Resources/HellionStrings.hu.resx index 89231d5..7884c81 100644 --- a/HellionChat/Resources/HellionStrings.hu.resx +++ b/HellionChat/Resources/HellionStrings.hu.resx @@ -1213,4 +1213,7 @@ Hivatkozott tárgy beszúrása <item> + + kikapcsolva + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.it.resx b/HellionChat/Resources/HellionStrings.it.resx index e21a16f..ec73987 100644 --- a/HellionChat/Resources/HellionStrings.it.resx +++ b/HellionChat/Resources/HellionStrings.it.resx @@ -1214,4 +1214,7 @@ Inserisci l'oggetto collegato <item> + + disattivato + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.ja.resx b/HellionChat/Resources/HellionStrings.ja.resx index 99fa915..8a02c96 100644 --- a/HellionChat/Resources/HellionStrings.ja.resx +++ b/HellionChat/Resources/HellionStrings.ja.resx @@ -1214,4 +1214,7 @@ リンクしたアイテムを挿入 <item> + + オフ + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.ko.resx b/HellionChat/Resources/HellionStrings.ko.resx index 645788a..c07c4bf 100644 --- a/HellionChat/Resources/HellionStrings.ko.resx +++ b/HellionChat/Resources/HellionStrings.ko.resx @@ -1214,4 +1214,7 @@ 연결된 아이템 삽입 <item> + + 끔 + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.nb.resx b/HellionChat/Resources/HellionStrings.nb.resx index c9e9ee5..43f0671 100644 --- a/HellionChat/Resources/HellionStrings.nb.resx +++ b/HellionChat/Resources/HellionStrings.nb.resx @@ -1213,4 +1213,7 @@ Sett inn lenket gjenstand <item> + + av + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.nl.resx b/HellionChat/Resources/HellionStrings.nl.resx index 215be2f..77f653c 100644 --- a/HellionChat/Resources/HellionStrings.nl.resx +++ b/HellionChat/Resources/HellionStrings.nl.resx @@ -1214,4 +1214,7 @@ Gekoppeld voorwerp invoegen <item> + + uit + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.pl.resx b/HellionChat/Resources/HellionStrings.pl.resx index 7d02ae7..5fa9e63 100644 --- a/HellionChat/Resources/HellionStrings.pl.resx +++ b/HellionChat/Resources/HellionStrings.pl.resx @@ -1213,4 +1213,7 @@ Wstaw powiązany przedmiot <item> + + wyłączone + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.pt-BR.resx b/HellionChat/Resources/HellionStrings.pt-BR.resx index d97fa64..8544c04 100644 --- a/HellionChat/Resources/HellionStrings.pt-BR.resx +++ b/HellionChat/Resources/HellionStrings.pt-BR.resx @@ -1214,4 +1214,7 @@ Inserir item vinculado <item> + + desativado + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.pt-PT.resx b/HellionChat/Resources/HellionStrings.pt-PT.resx index 9e9813b..40c813b 100644 --- a/HellionChat/Resources/HellionStrings.pt-PT.resx +++ b/HellionChat/Resources/HellionStrings.pt-PT.resx @@ -1213,4 +1213,7 @@ Inserir item ligado <item> + + desativado + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.resx b/HellionChat/Resources/HellionStrings.resx index 5e4e12f..9d91d8c 100644 --- a/HellionChat/Resources/HellionStrings.resx +++ b/HellionChat/Resources/HellionStrings.resx @@ -1225,4 +1225,7 @@ The game reports a failed tell in the log only. This raises a notification instead, so a message to someone offline or on another world does not go unnoticed. + + off + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.ro.resx b/HellionChat/Resources/HellionStrings.ro.resx index eed5fcb..66138da 100644 --- a/HellionChat/Resources/HellionStrings.ro.resx +++ b/HellionChat/Resources/HellionStrings.ro.resx @@ -1214,4 +1214,7 @@ Inserează obiectul legat <item> + + dezactivat + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.ru.resx b/HellionChat/Resources/HellionStrings.ru.resx index 96e5a9b..097e003 100644 --- a/HellionChat/Resources/HellionStrings.ru.resx +++ b/HellionChat/Resources/HellionStrings.ru.resx @@ -1214,4 +1214,7 @@ Вставить связанный предмет <item> + + выключено + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.sv.resx b/HellionChat/Resources/HellionStrings.sv.resx index f563572..7576dea 100644 --- a/HellionChat/Resources/HellionStrings.sv.resx +++ b/HellionChat/Resources/HellionStrings.sv.resx @@ -1214,4 +1214,7 @@ Infoga länkat föremål <item> + + av + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.tr.resx b/HellionChat/Resources/HellionStrings.tr.resx index 14c9e78..c8d2658 100644 --- a/HellionChat/Resources/HellionStrings.tr.resx +++ b/HellionChat/Resources/HellionStrings.tr.resx @@ -1213,4 +1213,7 @@ Bağlantılı eşyayı ekle <item> + + kapalı + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.uk.resx b/HellionChat/Resources/HellionStrings.uk.resx index 2a49c92..8ac78e7 100644 --- a/HellionChat/Resources/HellionStrings.uk.resx +++ b/HellionChat/Resources/HellionStrings.uk.resx @@ -1213,4 +1213,7 @@ Вставити пов'язаний предмет <item> + + вимкнено + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.zh-Hans.resx b/HellionChat/Resources/HellionStrings.zh-Hans.resx index 0264a9c..95890cd 100644 --- a/HellionChat/Resources/HellionStrings.zh-Hans.resx +++ b/HellionChat/Resources/HellionStrings.zh-Hans.resx @@ -1214,4 +1214,7 @@ 插入关联物品 <item> + + 关闭 + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.zh-Hant.resx b/HellionChat/Resources/HellionStrings.zh-Hant.resx index 5f84eaf..4dbda0a 100644 --- a/HellionChat/Resources/HellionStrings.zh-Hant.resx +++ b/HellionChat/Resources/HellionStrings.zh-Hant.resx @@ -1214,4 +1214,7 @@ 插入關聯物品 <item> + + 關閉 + \ No newline at end of file diff --git a/HellionChat/SelfTests/ConfigMigrationV24Step.cs b/HellionChat/SelfTests/ConfigMigrationV25Step.cs similarity index 92% rename from HellionChat/SelfTests/ConfigMigrationV24Step.cs rename to HellionChat/SelfTests/ConfigMigrationV25Step.cs index 1d44dee..3219a13 100644 --- a/HellionChat/SelfTests/ConfigMigrationV24Step.cs +++ b/HellionChat/SelfTests/ConfigMigrationV25Step.cs @@ -8,20 +8,20 @@ namespace HellionChat.SelfTests; // below must carry valid values here. This probe never rewrites config; the // migrations themselves are load-time and verified by the prepared-config smoke // in the plan. -internal sealed class ConfigMigrationV24Step : ISelfTestStep +internal sealed class ConfigMigrationV25Step : ISelfTestStep { - public ConfigMigrationV24Step(Plugin plugin) + public ConfigMigrationV25Step(Plugin plugin) { _ = plugin; } - public string Name => "Hellion Chat - Config v24 migration"; + public string Name => "Hellion Chat - Config v25 migration"; public SelfTestStepResult RunStep() { - if (Plugin.Config.Version != 24) + if (Plugin.Config.Version != 25) { - ImGui.Text($"Config.Version is {Plugin.Config.Version}, expected 24"); + ImGui.Text($"Config.Version is {Plugin.Config.Version}, expected 25"); return SelfTestStepResult.Fail; } diff --git a/HellionChat/Ui/AutoTellTabTint.cs b/HellionChat/Ui/AutoTellTabTint.cs new file mode 100644 index 0000000..70035c3 --- /dev/null +++ b/HellionChat/Ui/AutoTellTabTint.cs @@ -0,0 +1,97 @@ +namespace HellionChat.Ui; + +// Deterministic hash-based color and icon tinting for Auto-Tell sidebar tabs. +// Same tell partner (name+world) always produces the same color and icon across +// sessions. Pure string logic, no Dalamud dependency — testable without game refs. +internal static class AutoTellTabTint +{ + // Fallback for invalid input (empty name or world=0). White matches + // TextPrimary default so the sidebar stays visually consistent. + public const uint Fallback = 0xFFFFFFFFu; + + // 12 saturated mid-bright colors from the built-in theme pool, readable + // on dark backgrounds. Collision risk is low at realistic 1-5 active tells. + // RGBA format, matching ColourUtil.RgbaToAbgr convention. + public static readonly IReadOnlyList Palette = new uint[] + { + 0x00BED2FFu, // Arctic Cyan + 0xF97316FFu, // Ember Orange + 0xB585FFFFu, // Light Cosmic Purple + 0xE374E8FFu, // Bloom Magenta + 0x5DD39EFFu, // Mint Green + 0xF0AD4EFFu, // Warning Yellow + 0xE85C6AFFu, // Coral + 0x5CB85CFFu, // Status Green + 0x6278FFFFu, // Bloom Blue + 0xC9982EFFu, // Warm Gold + 0x9CCB7CFFu, // Soft Sage + 0xE85D04FFu, // Deep Ember + }; + + public static uint For(string name, uint world) + { + if (string.IsNullOrEmpty(name) || world == 0) + return Fallback; + + return Palette[(int)(StableHash($"{name}@{world}") % Palette.Count)]; + } + + // 7 visually distinct FA glyphs that make sense in a tell context. + // Excludes cog/comment/users — those read as system or group tabs. + public static readonly IReadOnlyList IconPool = new[] + { + "envelope", + "star", + "heart", + "bell", + "bookmark", + "flag", + "fire", + }; + + // "envelope" matches the tell context better than the old hardcoded "clock". + public const string IconFallback = "envelope"; + + public static string IconFor(string name, uint world) + { + if (string.IsNullOrEmpty(name) || world == 0) + return IconFallback; + + // Reversed key ("world@name") gives icon and color independent variation + // so the same tell partner doesn't always get the same color+icon pair. + // 7 icons x 12 colors = 84 distinct combinations. + return IconPool[(int)(StableHash($"{world}@{name}") % IconPool.Count)]; + } + + // FNV-1a, not string.GetHashCode. The header of this file promises the same + // partner produces the same colour "across sessions", and GetHashCode cannot + // keep that: .NET salts string hashing per process, so every game start + // would reshuffle every tell tab. The tests never caught it because they + // only ever compared two calls inside one run. + // Returns the full uint. The old int-based version masked off the sign bit + // before its modulo; on a uint that mask only throws away a bit of entropy. + private static uint StableHash(string key) + { + const uint offsetBasis = 2166136261u; + const uint prime = 16777619u; + + var hash = offsetBasis; + foreach (var b in System.Text.Encoding.UTF8.GetBytes(key)) + { + hash ^= b; + hash *= prime; + } + + // Avalanche step, and not optional. FNV-1a alone leaves the low bits + // correlated for keys that differ only slightly, and the caller takes + // exactly those bits with a modulo -- a probe over 144 near-identical + // keys reached only 6 of the 12 colours. With fmix32 it reaches all 12. + hash ^= hash >> 16; + hash *= 0x7feb352du; + hash ^= hash >> 15; + hash *= 0x846ca68bu; + hash ^= hash >> 16; + + return hash; + } +} diff --git a/HellionChat/Ui/Components/MessageList.cs b/HellionChat/Ui/Components/MessageList.cs index 67982eb..e9fc90a 100644 --- a/HellionChat/Ui/Components/MessageList.cs +++ b/HellionChat/Ui/Components/MessageList.cs @@ -37,9 +37,10 @@ internal sealed class MessageList private readonly Action _drawCompactRow; private readonly Action _drawCardRow; - // Reused across frames: at the default MaxLinesToRender of 2500 a fresh - // array per frame is 10 KB of garbage, and A2 put the default density on - // this path. + // Reused across frames: at MessageManager.MessageDisplayLimit a fresh array + // per frame is 40 KB of garbage, and A2 put the default density on this + // path. The old comment named MaxLinesToRender and its 2500 default, a + // config field that had stopped bounding anything. private float[] _heightScratch = []; // §6.2: setter-injection breaks the PayloadHandler → MainWindow → MessageList → PayloadHandler 3-cycle. diff --git a/HellionChat/Ui/Components/Settings/Tabs/WindowTab.cs b/HellionChat/Ui/Components/Settings/Tabs/WindowTab.cs index 03849cc..4ca54ba 100644 --- a/HellionChat/Ui/Components/Settings/Tabs/WindowTab.cs +++ b/HellionChat/Ui/Components/Settings/Tabs/WindowTab.cs @@ -121,7 +121,13 @@ internal sealed class WindowTab ); } - if (_w.Section(ImGui.GetID("window.preview"u8), "Input preview", open: false)) + if ( + _w.Section( + ImGui.GetID("window.preview"u8), + HellionStrings.Settings_Section_InputPreview, + open: false + ) + ) { _w.EnumComboRow( ImGui.GetID("window.preview.position"u8), @@ -186,6 +192,31 @@ internal sealed class WindowTab () => Plugin.Config.HideInNewGamePlusMenu, v => Plugin.Config.HideInNewGamePlusMenu = v ); + + // The three that lost their reader with the chat window. Pressing + // the chat key during a cutscene brings the chat back for that + // cutscene; combat has no such escape, exactly as in v1.5.6. + _w.ToggleRow( + ImGui.GetID("window.hide.cutscenes"u8), + Language.Options_HideDuringCutscenes_Name, + Language.Options_HideDuringCutscenes_Description, + () => Plugin.Config.HideDuringCutscenes, + v => Plugin.Config.HideDuringCutscenes = v + ); + _w.ToggleRow( + ImGui.GetID("window.hide.battle"u8), + Language.Options_HideInBattle_Name, + Language.Options_HideInBattle_Description, + () => Plugin.Config.HideInBattle, + v => Plugin.Config.HideInBattle = v + ); + _w.ToggleRow( + ImGui.GetID("window.hide.notloggedin"u8), + Language.Options_HideWhenNotLoggedIn_Name, + Language.Options_HideWhenNotLoggedIn_Description, + () => Plugin.Config.HideWhenNotLoggedIn, + v => Plugin.Config.HideWhenNotLoggedIn = v + ); } if ( diff --git a/HellionChat/Ui/Components/Sidebar.cs b/HellionChat/Ui/Components/Sidebar.cs index 179eaa5..b0b4a02 100644 --- a/HellionChat/Ui/Components/Sidebar.cs +++ b/HellionChat/Ui/Components/Sidebar.cs @@ -324,10 +324,17 @@ internal sealed class Sidebar var icon = ResolveTabIcon(tab); - // Dim precedence (1.5.6): the active tab always keeps its regular - // color; only greeted, non-active tabs drop to TextDim. + // Dim precedence (1.5.6): the active tab never dims; only greeted, + // non-active tabs drop to TextDim. "Regular colour" is the tint for an + // auto-tell tab and the theme text colour for everything else. + // + // Below that, an auto-tell tab is tinted from its partner. Twelve + // colours against seven glyphs is 84 combinations, which is plenty for + // the one to five conversations anybody actually runs in parallel. The + // greeted dim still wins: it says something about this tab right now, + // the tint only says who it belongs to. var isCurrentTab = tab == activeTab; - var iconColor = textAbgr; + var iconColor = tab.IsTempTab ? ColourUtil.RgbaToAbgr(TabTintCache.GetTint(tab)) : textAbgr; if ( !isCurrentTab && greetedConfigured @@ -486,10 +493,20 @@ internal sealed class Sidebar ) return mapped; - // Auto-tell tabs always show the envelope, regardless of what their - // SelectedChannels filter is set to. + // Auto-tell tabs get one of seven glyphs derived from the partner, not + // one envelope for all of them. With four tells open, identical rows in + // identical colour are four rows you have to read to tell apart. if (tab.IsTempTab) - return FontAwesomeIcon.Envelope; + { + // TryGetValue rather than an indexer: every glyph in the pool is in + // the table today, and a lookup miss would be somebody editing one + // of the two lists without the other. A wrong envelope beats a + // KeyNotFoundException on the draw thread. + var hashed = TabTintCache.GetIcon(tab); + return IconByName.TryGetValue(hashed, out var tellGlyph) + ? tellGlyph + : FontAwesomeIcon.Envelope; + } // Channel-type fallback. Walk every selected key, not just the first, // so a System tab that filters multiple system-flavoured ChatTypes diff --git a/HellionChat/Ui/FirstRunWizard.cs b/HellionChat/Ui/FirstRunWizard.cs index 240badf..ea2017d 100644 --- a/HellionChat/Ui/FirstRunWizard.cs +++ b/HellionChat/Ui/FirstRunWizard.cs @@ -483,10 +483,16 @@ public sealed class FirstRunWizard : Window string.Format(HellionStrings.Wizard_Step4_Summary_Profile, profileLabel) ); - var historyLabel = - (_state.PendingFilterIncludePreviousSessions ?? false) - ? HellionStrings.Wizard_Step3_FilterIncludePreviousSessions_Label - : HellionStrings.Wizard_Step4_Summary_Unchanged; + // Three states, not two. "Unchanged" is for a step the user + // never entered; an unticked box is a decision that gets + // committed, and reporting it as unchanged was the same kind of + // lie the deleted LoadPreviousSession told. + var historyLabel = _state.PendingFilterIncludePreviousSessions switch + { + true => HellionStrings.Wizard_Step3_FilterIncludePreviousSessions_Label, + false => HellionStrings.Wizard_Step4_Summary_Off, + null => HellionStrings.Wizard_Step4_Summary_Unchanged, + }; ImGui.TextWrapped( string.Format(HellionStrings.Wizard_Step4_Summary_History, historyLabel) ); diff --git a/HellionChat/Ui/TabTintCache.cs b/HellionChat/Ui/TabTintCache.cs new file mode 100644 index 0000000..5364ca4 --- /dev/null +++ b/HellionChat/Ui/TabTintCache.cs @@ -0,0 +1,38 @@ +namespace HellionChat.Ui; + +// Per-Tab cache wrapper around the pure AutoTellTabTint hash helpers. +// Each cache (tint, icon) carries its own name+world validation key so +// neither read path mutates the other's state — refilling one never +// invalidates the other. No string allocation in the steady-state lookup. +internal static class TabTintCache +{ + public static uint GetTint(Tab tab) + { + var name = tab.TellTarget.Name; + var world = tab.TellTarget.World; + if (tab._cachedTintTellName != name || tab._cachedTintTellWorld != world) + { + tab._cachedTintTellName = name; + tab._cachedTintTellWorld = world; + tab._cachedTellTint = AutoTellTabTint.For(name, world); + } + return tab._cachedTellTint; + } + + public static string GetIcon(Tab tab) + { + var name = tab.TellTarget.Name; + var world = tab.TellTarget.World; + if ( + tab._cachedTellIcon is null + || tab._cachedIconTellName != name + || tab._cachedIconTellWorld != world + ) + { + tab._cachedIconTellName = name; + tab._cachedIconTellWorld = world; + tab._cachedTellIcon = AutoTellTabTint.IconFor(name, world); + } + return tab._cachedTellIcon; + } +} diff --git a/HellionChat/Ui/Windows/MainWindow.cs b/HellionChat/Ui/Windows/MainWindow.cs index 737d73c..9684f62 100644 --- a/HellionChat/Ui/Windows/MainWindow.cs +++ b/HellionChat/Ui/Windows/MainWindow.cs @@ -226,6 +226,10 @@ internal sealed class MainWindow : Window, IFocusableChatWindow internal void ActivateChat() { _userHidden = false; + + // Also lifts a cutscene hide for the duration of that cutscene. Without + // this the key would appear to do nothing at all during one. + Plugin.Instance.ChatActivationRequested = true; if (!IsOpen) { IsOpen = true; diff --git a/HellionChat/Util/ChatHideState.cs b/HellionChat/Util/ChatHideState.cs new file mode 100644 index 0000000..1c99b88 --- /dev/null +++ b/HellionChat/Util/ChatHideState.cs @@ -0,0 +1,76 @@ +namespace HellionChat.Util; + +// Why the chat is currently hidden, if it is. +internal enum ChatHideReason +{ + None, + + // In combat, and the user asked for that to hide the chat. + Battle, + + // A cutscene or gpose is running. + Cutscene, + + // A cutscene is running and the user pressed the activation key anyway. + // Distinct from None so the state returns to hidden on its own once the + // cutscene ends, without a second gesture. + CutsceneOverride, +} + +// The hide conditions v1.5.6 evaluated every frame, as a state machine. +// +// Three of the four went missing with the chat window in cf4705e: their config +// fields survived, their translated labels survived, nothing read them. They +// need a machine rather than three ifs because two of them are not conditions +// but states: a cutscene the user has dismissed must stay dismissed until the +// cutscene ends, and combat that started while the chat was already hidden for +// another reason must not take ownership of it. +// +// Pure and Dalamud-free, so the transitions can be pinned without a game. +internal static class ChatHideState +{ + internal readonly record struct Inputs( + bool HideInBattle, + bool InBattle, + bool HideDuringCutscenes, + bool CutsceneActive, + bool ActivateRequested + ); + + internal static ChatHideReason Next(ChatHideReason current, in Inputs inputs) + { + // Leaving a state comes first. Otherwise a frame in which combat ends + // and a cutscene starts would keep reporting Battle. + if (current == ChatHideReason.Battle && !inputs.InBattle) + current = ChatHideReason.None; + + if ( + current is ChatHideReason.Cutscene or ChatHideReason.CutsceneOverride + && !inputs.CutsceneActive + ) + current = ChatHideReason.None; + + // The user asked for the chat during a cutscene. Held until the cutscene + // ends, which the branch above takes care of. + if (current == ChatHideReason.Cutscene && inputs.ActivateRequested) + return ChatHideReason.CutsceneOverride; + + if (current != ChatHideReason.None) + return current; + + // Entering. Cutscene wins over battle: a cutscene during combat is the + // more specific situation, and it is the one with an escape hatch. + if (inputs.HideDuringCutscenes && inputs.CutsceneActive) + return ChatHideReason.Cutscene; + + if (inputs.HideInBattle && inputs.InBattle) + return ChatHideReason.Battle; + + return ChatHideReason.None; + } + + // CutsceneOverride is a hide state that does not hide -- that is the whole + // point of it. + internal static bool Hides(ChatHideReason reason) => + reason is ChatHideReason.Battle or ChatHideReason.Cutscene; +} diff --git a/HellionChat/Util/LayoutFingerprint.cs b/HellionChat/Util/LayoutFingerprint.cs index 22052ba..f6c6785 100644 --- a/HellionChat/Util/LayoutFingerprint.cs +++ b/HellionChat/Util/LayoutFingerprint.cs @@ -21,8 +21,8 @@ internal readonly record struct LayoutFingerprint( // Dragging a window edge or the Dalamud UI-scale slider moves the continuous // half of the fingerprint on every frame. Acting on each one drops the height // cache and sends the whole tab through the linear measure path (up to -// Config.MaxLinesToRender rows). Waiting for those to settle turns a drag into -// one rebuild. Discrete changes bypass the wait entirely. +// MessageManager.MessageDisplayLimit rows). Waiting for those to settle turns a +// drag into one rebuild. Discrete changes bypass the wait entirely. internal sealed class LayoutFingerprintGate { internal const long SettleMs = 200; diff --git a/HellionChat/Util/TabLifecycleHelpers.cs b/HellionChat/Util/TabLifecycleHelpers.cs index fb0a4f3..093eec4 100644 --- a/HellionChat/Util/TabLifecycleHelpers.cs +++ b/HellionChat/Util/TabLifecycleHelpers.cs @@ -22,7 +22,7 @@ internal static class TabLifecycleHelpers // with no window. Unconditional (pinned included) because pinned TempTabs // survive the load and are the main stale-flag source; a !IsPinned filter // would leave exactly those leaking. Lockstep with the two in-memory resets - // (AutoTellTabsService pool-full + Configuration.UpdateFrom backToOriginal). + // (AutoTellTabsService pool-full + the settings round trip backToOriginal). // TEST-MIRROR: ../../../Hellion Build test/_Helpers/PopOutResetOnLoadTests.cs internal static void ResetPopOutOnLoad(IEnumerable tabs) {