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.
This commit is contained in:
2026-08-18 23:36:54 +02:00
parent 6829a80ff2
commit 8e38e3e805
39 changed files with 424 additions and 207 deletions
+97
View File
@@ -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;
}
}