refactor(config): delete what was never read, restore what was only orphaned
Block D of v1.12.0. The line between the two is the whole job, and I got it wrong once on the way: six cache fields on Tab looked dead because nothing writes them, and nothing writes them because AutoTellTabTint and TabTintCache went out with the chat window incf4705e. Deleting the fields would have cemented a loss instead of recording a decision. So they are back, and the sidebar uses them again: an auto-tell tab is tinted and glyphed from its partner, twelve colours against seven icons. Four open tells are no longer four identical envelopes in one colour. Their own header promised the same partner keeps its colour "across sessions" while hashing with string.GetHashCode, which .NET salts per process -- every game start reshuffled every tab. FNV-1a now, with a lowbias32 finalizer that is not decoration: without it a probe over 144 similar keys reached six of the twelve colours, because the caller takes the low bits with a modulo and FNV leaves those correlated. Three pinned values guard it, which is also the only assertion that can catch a regression to a salted hash. The same question, asked of the three hide conditions this block had quietly orphaned: HideDuringCutscenes, HideInBattle, HideWhenNotLoggedIn all had readers in v1.5.6 and lost them in the same commit. Two of them are states rather than conditions -- a cutscene the user dismissed stays dismissed until it ends, and combat must not seize a chat that is already hidden for another reason -- so they come back as a small state machine with eight pinned transitions, and three toggles whose labels were already translated in all 25 languages. Actually deleted, with a reader search each time: - Six per-tab hide fields. Their reader was the pop-out window and it stopped consulting them incf4705e. Per-tab was the wrong unit anyway: "hide during cutscenes" is a statement about the screen. - Tab.ChatCodes, whose migration the v16 schema gate had already made unreachable. - InactivityHideTimeout and InactivityHideActiveDuringBattle, MaxLinesToRender which had stopped bounding anything, and the 155 lines of Configuration.UpdateFrom with no caller at all. Config version 25, at all three places that carry it. No migration step: the gate only refuses anything under 16 and Json.NET drops keys it does not know, so the deleted fields simply stop being written. One thing a review pass caught that matters more than any of the above: the clone parity guard had gone hollow. It compares collections by count, ChatCodes was the only collection the probe seeded, and removing it left the guard comparing zero against zero. Verified by making Tab.Clone discard both remaining collections and watching every assertion stay green. The probe seeds them now, and the same sabotage fails as it should.
This commit is contained in:
+14
-183
@@ -35,7 +35,7 @@ public class ConfigKeyBind
|
|||||||
[Serializable]
|
[Serializable]
|
||||||
public class Configuration : IPluginConfiguration
|
public class Configuration : IPluginConfiguration
|
||||||
{
|
{
|
||||||
internal const int LatestVersion = 24;
|
internal const int LatestVersion = 25;
|
||||||
|
|
||||||
public int Version { get; set; } = LatestVersion;
|
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.
|
// v1.2.1: default flipped false → true for consistency with other hide defaults.
|
||||||
public bool HideInNewGamePlusMenu = true;
|
public bool HideInNewGamePlusMenu = true;
|
||||||
public bool HideWhenInactive;
|
public bool HideWhenInactive;
|
||||||
public int InactivityHideTimeout = 10;
|
|
||||||
public bool InactivityHideActiveDuringBattle = true;
|
|
||||||
|
|
||||||
[Obsolete("Use InactivityHideChannelsV2 instead")]
|
[Obsolete("Use InactivityHideChannelsV2 instead")]
|
||||||
public Dictionary<ChatType, ChatSource> InactivityHideChannels = [];
|
public Dictionary<ChatType, ChatSource> InactivityHideChannels = [];
|
||||||
@@ -243,7 +241,6 @@ public class Configuration : IPluginConfiguration
|
|||||||
// UI-11: warn before sending a message that carries plugin-only glyphs.
|
// UI-11: warn before sending a message that carries plugin-only glyphs.
|
||||||
public bool NotifyPluginDisclosure = true;
|
public bool NotifyPluginDisclosure = true;
|
||||||
public bool KeepInputFocus = true;
|
public bool KeepInputFocus = true;
|
||||||
public int MaxLinesToRender = 2_500; // 1-10000
|
|
||||||
public bool Use24HourClock = true;
|
public bool Use24HourClock = true;
|
||||||
public bool ShowEmotes = true;
|
public bool ShowEmotes = true;
|
||||||
public HashSet<string> BlockedEmotes = [];
|
public HashSet<string> BlockedEmotes = [];
|
||||||
@@ -310,164 +307,6 @@ public class Configuration : IPluginConfiguration
|
|||||||
// v22 field: MainWindow layout mode (sidebar vs. horizontal top tabs).
|
// v22 field: MainWindow layout mode (sidebar vs. horizontal top tabs).
|
||||||
// Initializer doubles as the migration default for configs loaded at v21.
|
// Initializer doubles as the migration default for configs loaded at v21.
|
||||||
public MainWindowLayoutMode MainWindowLayoutMode = MainWindowLayoutMode.Sidebar;
|
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<string>(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]
|
[Serializable]
|
||||||
@@ -540,9 +379,6 @@ public class Tab
|
|||||||
// Optional FontAwesome glyph name; null falls back to TabIconMapping default.
|
// Optional FontAwesome glyph name; null falls back to TabIconMapping default.
|
||||||
public string? Icon = null;
|
public string? Icon = null;
|
||||||
|
|
||||||
[Obsolete("Removed in favor of SelectedChannels")]
|
|
||||||
public Dictionary<ChatType, ChatSource> ChatCodes = new();
|
|
||||||
|
|
||||||
public Dictionary<ChatType, (ChatSource, ChatSource)> SelectedChannels = new();
|
public Dictionary<ChatType, (ChatSource, ChatSource)> SelectedChannels = new();
|
||||||
public bool ExtraChatAll;
|
public bool ExtraChatAll;
|
||||||
public HashSet<Guid> ExtraChatChannels = [];
|
public HashSet<Guid> ExtraChatChannels = [];
|
||||||
@@ -559,12 +395,15 @@ public class Tab
|
|||||||
public bool CanMove = true;
|
public bool CanMove = true;
|
||||||
public bool CanResize = true;
|
public bool CanResize = true;
|
||||||
|
|
||||||
public bool IndependentHide;
|
// Six per-tab hide conditions used to live here. Their reader was the
|
||||||
public bool HideDuringCutscenes = true;
|
// pop-out window, which stopped consulting them in cf4705e; the equivalents
|
||||||
public bool HideWhenNotLoggedIn = true;
|
// that survive are the window-level fields of the same name further up this
|
||||||
public bool HideWhenUiHidden = true;
|
// file, and v1.12.0 gave every one of those a control.
|
||||||
public bool HideInLoadingScreens;
|
//
|
||||||
public bool HideInBattle;
|
// 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 HideWhenInactive;
|
||||||
|
|
||||||
public bool IsTempTab;
|
public bool IsTempTab;
|
||||||
@@ -667,15 +506,14 @@ public class Tab
|
|||||||
|
|
||||||
public Tab Clone()
|
public Tab Clone()
|
||||||
{
|
{
|
||||||
#pragma warning disable CS0618 // ChatCodes is obsolete but still serialized and must survive a clone
|
|
||||||
return new Tab
|
return new Tab
|
||||||
{
|
{
|
||||||
Name = Name,
|
Name = Name,
|
||||||
// Both were missing: Icon feeds the sidebar glyph, ChatCodes carries
|
// Icon feeds the sidebar glyph and a clone round-trip used to drop
|
||||||
// legacy migration data that is still written to the JSON. A clone
|
// it silently. ChatCodes sat beside it until v1.12.0, carrying data
|
||||||
// round-trip used to drop them silently.
|
// for a migration that the v16 schema gate had already made
|
||||||
|
// unreachable.
|
||||||
Icon = Icon,
|
Icon = Icon,
|
||||||
ChatCodes = new Dictionary<ChatType, ChatSource>(ChatCodes),
|
|
||||||
SelectedChannels = SelectedChannels.ToDictionary(pair => pair.Key, pair => pair.Value),
|
SelectedChannels = SelectedChannels.ToDictionary(pair => pair.Key, pair => pair.Value),
|
||||||
ExtraChatAll = ExtraChatAll,
|
ExtraChatAll = ExtraChatAll,
|
||||||
ExtraChatChannels = ExtraChatChannels.ToHashSet(),
|
ExtraChatChannels = ExtraChatChannels.ToHashSet(),
|
||||||
@@ -693,12 +531,6 @@ public class Tab
|
|||||||
CurrentChannel = CurrentChannel.Clone(),
|
CurrentChannel = CurrentChannel.Clone(),
|
||||||
CanMove = CanMove,
|
CanMove = CanMove,
|
||||||
CanResize = CanResize,
|
CanResize = CanResize,
|
||||||
IndependentHide = IndependentHide,
|
|
||||||
HideDuringCutscenes = HideDuringCutscenes,
|
|
||||||
HideWhenNotLoggedIn = HideWhenNotLoggedIn,
|
|
||||||
HideWhenUiHidden = HideWhenUiHidden,
|
|
||||||
HideInLoadingScreens = HideInLoadingScreens,
|
|
||||||
HideInBattle = HideInBattle,
|
|
||||||
HideWhenInactive = HideWhenInactive,
|
HideWhenInactive = HideWhenInactive,
|
||||||
IsTempTab = IsTempTab,
|
IsTempTab = IsTempTab,
|
||||||
IsPinned = IsPinned,
|
IsPinned = IsPinned,
|
||||||
@@ -708,7 +540,6 @@ public class Tab
|
|||||||
NotificationSoundId = NotificationSoundId,
|
NotificationSoundId = NotificationSoundId,
|
||||||
IsGreeted = IsGreeted,
|
IsGreeted = IsGreeted,
|
||||||
};
|
};
|
||||||
#pragma warning restore CS0618
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ordered message list with duplicate ID tracking, sorting and mutex protection.
|
/// Ordered message list with duplicate ID tracking, sorting and mutex protection.
|
||||||
|
|||||||
+42
-2
@@ -167,6 +167,14 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
// via Framework.RunOnTick (v1.4.8 B3 retention sweep) can early-bail
|
// via Framework.RunOnTick (v1.4.8 B3 retention sweep) can early-bail
|
||||||
// before they touch state that has already been torn down. Volatile
|
// before they touch state that has already been torn down. Volatile
|
||||||
// because the tick reads it from a different thread than the writer.
|
// 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;
|
private volatile bool _isDisposing;
|
||||||
|
|
||||||
// Read by background workers that outlive a teardown -- the export thread
|
// 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
|
// Unpinned TempTabs are session-only and dropped on every load. Pinned
|
||||||
// TempTabs survive reload — Jin's tester feedback (v1.4.7).
|
// 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.SettingsWindowOpenStep(this),
|
||||||
new SelfTests.OnOpenMainUiRoutesMainWindowStep(this),
|
new SelfTests.OnOpenMainUiRoutesMainWindowStep(this),
|
||||||
new SelfTests.TypingIpcStateStep(this),
|
new SelfTests.TypingIpcStateStep(this),
|
||||||
new SelfTests.ConfigMigrationV24Step(this),
|
new SelfTests.ConfigMigrationV25Step(this),
|
||||||
new SelfTests.ChannelPopoutBindStep(this),
|
new SelfTests.ChannelPopoutBindStep(this),
|
||||||
new SelfTests.HoverStateFootprintStep(),
|
new SelfTests.HoverStateFootprintStep(),
|
||||||
new SelfTests.HonorificHeaderRenderStep(this),
|
new SelfTests.HonorificHeaderRenderStep(this),
|
||||||
@@ -1203,6 +1217,32 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
|
|
||||||
Interface.UiBuilder.DisableUserUiHide = !Config.HideWhenUiHidden;
|
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
|
// RegularFont is nullable only because the live rebuild path
|
||||||
// disposes it before reassigning; both ends of that swap happen on
|
// disposes it before reassigning; both ends of that swap happen on
|
||||||
// this same draw thread, so it cannot be null here.
|
// this same draw thread, so it cannot be null here.
|
||||||
|
|||||||
@@ -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_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_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_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_TestHint => Get(nameof(Wizard_Step4_TestHint));
|
||||||
internal static string Wizard_Step4_SettingsHint => Get(nameof(Wizard_Step4_SettingsHint));
|
internal static string Wizard_Step4_SettingsHint => Get(nameof(Wizard_Step4_SettingsHint));
|
||||||
|
|
||||||
|
|||||||
@@ -1214,4 +1214,7 @@
|
|||||||
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
||||||
<value>Insereix l'objecte enllaçat <item></value>
|
<value>Insereix l'objecte enllaçat <item></value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_Off" xml:space="preserve">
|
||||||
|
<value>desactivat</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1213,4 +1213,7 @@
|
|||||||
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
||||||
<value>Vložit odkázaný předmět <item></value>
|
<value>Vložit odkázaný předmět <item></value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_Off" xml:space="preserve">
|
||||||
|
<value>vypnuto</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1213,4 +1213,7 @@
|
|||||||
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
||||||
<value>Indsæt linket genstand <item></value>
|
<value>Indsæt linket genstand <item></value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_Off" xml:space="preserve">
|
||||||
|
<value>fra</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1208,4 +1208,7 @@
|
|||||||
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
||||||
<value>Verlinkten Gegenstand einfügen <item></value>
|
<value>Verlinkten Gegenstand einfügen <item></value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_Off" xml:space="preserve">
|
||||||
|
<value>aus</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1213,4 +1213,7 @@
|
|||||||
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
||||||
<value>Εισαγωγή συνδεδεμένου αντικειμένου <item></value>
|
<value>Εισαγωγή συνδεδεμένου αντικειμένου <item></value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_Off" xml:space="preserve">
|
||||||
|
<value>ανενεργό</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1214,4 +1214,7 @@
|
|||||||
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
||||||
<value>Insertar objeto enlazado <item></value>
|
<value>Insertar objeto enlazado <item></value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_Off" xml:space="preserve">
|
||||||
|
<value>desactivado</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1213,4 +1213,7 @@
|
|||||||
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
||||||
<value>Lisää linkitetty esine <item></value>
|
<value>Lisää linkitetty esine <item></value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_Off" xml:space="preserve">
|
||||||
|
<value>pois</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1214,4 +1214,7 @@
|
|||||||
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
||||||
<value>Insérer l'objet lié <item></value>
|
<value>Insérer l'objet lié <item></value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_Off" xml:space="preserve">
|
||||||
|
<value>désactivé</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1213,4 +1213,7 @@
|
|||||||
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
||||||
<value>Hivatkozott tárgy beszúrása <item></value>
|
<value>Hivatkozott tárgy beszúrása <item></value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_Off" xml:space="preserve">
|
||||||
|
<value>kikapcsolva</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1214,4 +1214,7 @@
|
|||||||
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
||||||
<value>Inserisci l'oggetto collegato <item></value>
|
<value>Inserisci l'oggetto collegato <item></value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_Off" xml:space="preserve">
|
||||||
|
<value>disattivato</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1214,4 +1214,7 @@
|
|||||||
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
||||||
<value>リンクしたアイテムを挿入 <item></value>
|
<value>リンクしたアイテムを挿入 <item></value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_Off" xml:space="preserve">
|
||||||
|
<value>オフ</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1214,4 +1214,7 @@
|
|||||||
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
||||||
<value>연결된 아이템 삽입 <item></value>
|
<value>연결된 아이템 삽입 <item></value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_Off" xml:space="preserve">
|
||||||
|
<value>끔</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1213,4 +1213,7 @@
|
|||||||
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
||||||
<value>Sett inn lenket gjenstand <item></value>
|
<value>Sett inn lenket gjenstand <item></value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_Off" xml:space="preserve">
|
||||||
|
<value>av</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1214,4 +1214,7 @@
|
|||||||
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
||||||
<value>Gekoppeld voorwerp invoegen <item></value>
|
<value>Gekoppeld voorwerp invoegen <item></value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_Off" xml:space="preserve">
|
||||||
|
<value>uit</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1213,4 +1213,7 @@
|
|||||||
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
||||||
<value>Wstaw powiązany przedmiot <item></value>
|
<value>Wstaw powiązany przedmiot <item></value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_Off" xml:space="preserve">
|
||||||
|
<value>wyłączone</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1214,4 +1214,7 @@
|
|||||||
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
||||||
<value>Inserir item vinculado <item></value>
|
<value>Inserir item vinculado <item></value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_Off" xml:space="preserve">
|
||||||
|
<value>desativado</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1213,4 +1213,7 @@
|
|||||||
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
||||||
<value>Inserir item ligado <item></value>
|
<value>Inserir item ligado <item></value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_Off" xml:space="preserve">
|
||||||
|
<value>desativado</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1225,4 +1225,7 @@
|
|||||||
<data name="Settings_NotifyFailedTell_Description" xml:space="preserve">
|
<data name="Settings_NotifyFailedTell_Description" xml:space="preserve">
|
||||||
<value>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.</value>
|
<value>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.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_Off" xml:space="preserve">
|
||||||
|
<value>off</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1214,4 +1214,7 @@
|
|||||||
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
||||||
<value>Inserează obiectul legat <item></value>
|
<value>Inserează obiectul legat <item></value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_Off" xml:space="preserve">
|
||||||
|
<value>dezactivat</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1214,4 +1214,7 @@
|
|||||||
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
||||||
<value>Вставить связанный предмет <item></value>
|
<value>Вставить связанный предмет <item></value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_Off" xml:space="preserve">
|
||||||
|
<value>выключено</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1214,4 +1214,7 @@
|
|||||||
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
||||||
<value>Infoga länkat föremål <item></value>
|
<value>Infoga länkat föremål <item></value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_Off" xml:space="preserve">
|
||||||
|
<value>av</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1213,4 +1213,7 @@
|
|||||||
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
||||||
<value>Bağlantılı eşyayı ekle <item></value>
|
<value>Bağlantılı eşyayı ekle <item></value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_Off" xml:space="preserve">
|
||||||
|
<value>kapalı</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1213,4 +1213,7 @@
|
|||||||
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
||||||
<value>Вставити пов'язаний предмет <item></value>
|
<value>Вставити пов'язаний предмет <item></value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_Off" xml:space="preserve">
|
||||||
|
<value>вимкнено</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1214,4 +1214,7 @@
|
|||||||
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
||||||
<value>插入关联物品 <item></value>
|
<value>插入关联物品 <item></value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_Off" xml:space="preserve">
|
||||||
|
<value>关闭</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1214,4 +1214,7 @@
|
|||||||
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
||||||
<value>插入關聯物品 <item></value>
|
<value>插入關聯物品 <item></value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_Off" xml:space="preserve">
|
||||||
|
<value>關閉</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
+5
-5
@@ -8,20 +8,20 @@ namespace HellionChat.SelfTests;
|
|||||||
// below must carry valid values here. This probe never rewrites config; the
|
// below must carry valid values here. This probe never rewrites config; the
|
||||||
// migrations themselves are load-time and verified by the prepared-config smoke
|
// migrations themselves are load-time and verified by the prepared-config smoke
|
||||||
// in the plan.
|
// in the plan.
|
||||||
internal sealed class ConfigMigrationV24Step : ISelfTestStep
|
internal sealed class ConfigMigrationV25Step : ISelfTestStep
|
||||||
{
|
{
|
||||||
public ConfigMigrationV24Step(Plugin plugin)
|
public ConfigMigrationV25Step(Plugin plugin)
|
||||||
{
|
{
|
||||||
_ = plugin;
|
_ = plugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Name => "Hellion Chat - Config v24 migration";
|
public string Name => "Hellion Chat - Config v25 migration";
|
||||||
|
|
||||||
public SelfTestStepResult RunStep()
|
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;
|
return SelfTestStepResult.Fail;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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<uint> 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<string> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,9 +37,10 @@ internal sealed class MessageList
|
|||||||
private readonly Action<Message> _drawCompactRow;
|
private readonly Action<Message> _drawCompactRow;
|
||||||
private readonly Action<Message> _drawCardRow;
|
private readonly Action<Message> _drawCardRow;
|
||||||
|
|
||||||
// Reused across frames: at the default MaxLinesToRender of 2500 a fresh
|
// Reused across frames: at MessageManager.MessageDisplayLimit a fresh array
|
||||||
// array per frame is 10 KB of garbage, and A2 put the default density on
|
// per frame is 40 KB of garbage, and A2 put the default density on this
|
||||||
// this path.
|
// path. The old comment named MaxLinesToRender and its 2500 default, a
|
||||||
|
// config field that had stopped bounding anything.
|
||||||
private float[] _heightScratch = [];
|
private float[] _heightScratch = [];
|
||||||
|
|
||||||
// §6.2: setter-injection breaks the PayloadHandler → MainWindow → MessageList → PayloadHandler 3-cycle.
|
// §6.2: setter-injection breaks the PayloadHandler → MainWindow → MessageList → PayloadHandler 3-cycle.
|
||||||
|
|||||||
@@ -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(
|
_w.EnumComboRow(
|
||||||
ImGui.GetID("window.preview.position"u8),
|
ImGui.GetID("window.preview.position"u8),
|
||||||
@@ -186,6 +192,31 @@ internal sealed class WindowTab
|
|||||||
() => Plugin.Config.HideInNewGamePlusMenu,
|
() => Plugin.Config.HideInNewGamePlusMenu,
|
||||||
v => Plugin.Config.HideInNewGamePlusMenu = v
|
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 (
|
if (
|
||||||
|
|||||||
@@ -324,10 +324,17 @@ internal sealed class Sidebar
|
|||||||
|
|
||||||
var icon = ResolveTabIcon(tab);
|
var icon = ResolveTabIcon(tab);
|
||||||
|
|
||||||
// Dim precedence (1.5.6): the active tab always keeps its regular
|
// Dim precedence (1.5.6): the active tab never dims; only greeted,
|
||||||
// color; only greeted, non-active tabs drop to TextDim.
|
// 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 isCurrentTab = tab == activeTab;
|
||||||
var iconColor = textAbgr;
|
var iconColor = tab.IsTempTab ? ColourUtil.RgbaToAbgr(TabTintCache.GetTint(tab)) : textAbgr;
|
||||||
if (
|
if (
|
||||||
!isCurrentTab
|
!isCurrentTab
|
||||||
&& greetedConfigured
|
&& greetedConfigured
|
||||||
@@ -486,10 +493,20 @@ internal sealed class Sidebar
|
|||||||
)
|
)
|
||||||
return mapped;
|
return mapped;
|
||||||
|
|
||||||
// Auto-tell tabs always show the envelope, regardless of what their
|
// Auto-tell tabs get one of seven glyphs derived from the partner, not
|
||||||
// SelectedChannels filter is set to.
|
// 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)
|
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,
|
// Channel-type fallback. Walk every selected key, not just the first,
|
||||||
// so a System tab that filters multiple system-flavoured ChatTypes
|
// so a System tab that filters multiple system-flavoured ChatTypes
|
||||||
|
|||||||
@@ -483,10 +483,16 @@ public sealed class FirstRunWizard : Window
|
|||||||
string.Format(HellionStrings.Wizard_Step4_Summary_Profile, profileLabel)
|
string.Format(HellionStrings.Wizard_Step4_Summary_Profile, profileLabel)
|
||||||
);
|
);
|
||||||
|
|
||||||
var historyLabel =
|
// Three states, not two. "Unchanged" is for a step the user
|
||||||
(_state.PendingFilterIncludePreviousSessions ?? false)
|
// never entered; an unticked box is a decision that gets
|
||||||
? HellionStrings.Wizard_Step3_FilterIncludePreviousSessions_Label
|
// committed, and reporting it as unchanged was the same kind of
|
||||||
: HellionStrings.Wizard_Step4_Summary_Unchanged;
|
// 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(
|
ImGui.TextWrapped(
|
||||||
string.Format(HellionStrings.Wizard_Step4_Summary_History, historyLabel)
|
string.Format(HellionStrings.Wizard_Step4_Summary_History, historyLabel)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -226,6 +226,10 @@ internal sealed class MainWindow : Window, IFocusableChatWindow
|
|||||||
internal void ActivateChat()
|
internal void ActivateChat()
|
||||||
{
|
{
|
||||||
_userHidden = false;
|
_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)
|
if (!IsOpen)
|
||||||
{
|
{
|
||||||
IsOpen = true;
|
IsOpen = true;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -21,8 +21,8 @@ internal readonly record struct LayoutFingerprint(
|
|||||||
// Dragging a window edge or the Dalamud UI-scale slider moves the continuous
|
// 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
|
// 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
|
// 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
|
// MessageManager.MessageDisplayLimit rows). Waiting for those to settle turns a
|
||||||
// one rebuild. Discrete changes bypass the wait entirely.
|
// drag into one rebuild. Discrete changes bypass the wait entirely.
|
||||||
internal sealed class LayoutFingerprintGate
|
internal sealed class LayoutFingerprintGate
|
||||||
{
|
{
|
||||||
internal const long SettleMs = 200;
|
internal const long SettleMs = 200;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ internal static class TabLifecycleHelpers
|
|||||||
// with no window. Unconditional (pinned included) because pinned TempTabs
|
// with no window. Unconditional (pinned included) because pinned TempTabs
|
||||||
// survive the load and are the main stale-flag source; a !IsPinned filter
|
// 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
|
// 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
|
// TEST-MIRROR: ../../../Hellion Build test/_Helpers/PopOutResetOnLoadTests.cs
|
||||||
internal static void ResetPopOutOnLoad(IEnumerable<Tab> tabs)
|
internal static void ResetPopOutOnLoad(IEnumerable<Tab> tabs)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user