Files
HellionChat/HellionChat/Util/ChatHideState.cs
T
JonKazama-Hellion 8e38e3e805 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 in cf4705e. 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 in cf4705e. 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.
2026-08-18 23:36:54 +02:00

77 lines
2.7 KiB
C#

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;
}