chore: comments say what the code does, not which task produced it
A comment that reads "MUST stay in lockstep with TryGetActiveCrossfade (K8)" helps nobody outside the plan that used to have a K8 in it, and the plans are not in this repo. Same for "Spec FR-4", "plan §B.2", "Sub-Task 4.4" and the F/R/M/A/S round codes scattered through the style engine and the self-tests. Personal names go too. "tester feedback from Jin (v1.4.7)" and "Flo decision 2026-06-15" carry the reason fine without naming anyone -- the version and the reason are the parts a reader can act on, and a public repo should not need a cast list to be read. The rule applied throughout: keep the why, drop the reference. Version numbers stay, since those resolve through the changelog. 77 files. ChunkUtil also carried 281 lines of commented-out code -- an older ToChunks variant and two helpers with no callers, inherited and never removed. Deleted; git remembers them.
This commit is contained in:
@@ -58,8 +58,8 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
|
||||
// Derived from the tab list on read. Pin/Unpin/Promote/Logout simply
|
||||
// mutate IsPinned or remove tabs — the count adapts automatically.
|
||||
// Replaces the F2.1 Interlocked counter because the new pin-state
|
||||
// transitions are cold-path and don't need lock-free reads.
|
||||
// Replaces an Interlocked counter: the pin-state transitions are cold-path
|
||||
// and don't need lock-free reads.
|
||||
internal int ActiveTempTabCount =>
|
||||
Plugin.Config.Tabs.Count(TabLifecycleHelpers.IsInUnpinnedPool);
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ public class Configuration : IPluginConfiguration
|
||||
.PrivacyDefaults
|
||||
.DefaultPersistUnknownChannels;
|
||||
|
||||
// F3.2: dedup unknown-ChatType warnings so a chatty filter doesn't spam
|
||||
// Dedup unknown-ChatType warnings so a chatty filter doesn't spam
|
||||
// the log every frame. NonSerialized so the warning fires once per
|
||||
// runtime, not once-ever-per-install.
|
||||
[NonSerialized]
|
||||
@@ -107,7 +107,7 @@ public class Configuration : IPluginConfiguration
|
||||
|
||||
var known = Enum.IsDefined(typeof(ChatType), type);
|
||||
|
||||
// F3.2: log first occurrence of a ChatType the running build doesn't
|
||||
// Log the first occurrence of a ChatType the running build doesn't
|
||||
// recognise — i.e. one a future FFXIV patch may have added.
|
||||
if (!known && !listed && _warnedUnknownChannels.Add(type))
|
||||
{
|
||||
@@ -402,9 +402,9 @@ public class Tab
|
||||
|
||||
public bool IsTempTab;
|
||||
|
||||
// Pinned TempTabs survive plugin reload and logout — tester feedback from
|
||||
// Jin (v1.4.7). Pinned tabs live in their own pool (MaxPinnedTempTabs)
|
||||
// separate from the AutoTellTabsLimit bucket.
|
||||
// Pinned TempTabs survive plugin reload and logout -- tester feedback in
|
||||
// v1.4.7. Pinned tabs live in their own pool (MaxPinnedTempTabs) separate
|
||||
// from the AutoTellTabsLimit bucket.
|
||||
public bool IsPinned;
|
||||
public bool AllSenderMessages;
|
||||
public TellTarget TellTarget = TellTarget.Empty();
|
||||
|
||||
@@ -234,7 +234,7 @@ internal sealed unsafe class Chat : IDisposable
|
||||
|
||||
// Seed the just-typed character into our input field and focus it, the
|
||||
// same InputBar.AppendPending + Activate prefill path inventory item-links
|
||||
// use. Prefill-only — no tab switch (Flo decision 2026-06-15).
|
||||
// use. Prefill only, deliberately: no tab switch.
|
||||
if (input != null)
|
||||
{
|
||||
Plugin.InputBar.AppendPending(input);
|
||||
@@ -355,9 +355,9 @@ internal sealed unsafe class Chat : IDisposable
|
||||
if (playerName != null)
|
||||
{
|
||||
// Right-click -> Send Tell: prefill our input the same way our own
|
||||
// "Send Tell" payload menu does (PayloadHandler), then focus. Prefill-
|
||||
// only — no tab switch, no ChatActivatedArgs revival (Flo decision
|
||||
// 2026-06-15). The game supplies worldName here, so no sheet lookup.
|
||||
// "Send Tell" payload menu does (PayloadHandler), then focus. Prefill
|
||||
// only, deliberately: no tab switch, no ChatActivatedArgs revival.
|
||||
// The game supplies worldName here, so no sheet lookup.
|
||||
PrefillTellInput(
|
||||
playerName->ToString(),
|
||||
worldName != null ? worldName->ToString() : null
|
||||
@@ -393,7 +393,7 @@ internal sealed unsafe class Chat : IDisposable
|
||||
{
|
||||
// In-foray right-click -> Send Tell: same prefill path as the non-foray
|
||||
// tell. The foray-specific TellSpecial channel routing stays deferred
|
||||
// (v1.8.1, SetEurekaTellChannel) — prefill-only here (Flo decision 2026-06-15).
|
||||
// (v1.8.1, SetEurekaTellChannel) -- prefill only here as well.
|
||||
PrefillTellInput(
|
||||
playerName->ToString(),
|
||||
worldName != null ? worldName->ToString() : null
|
||||
|
||||
@@ -530,7 +530,7 @@ internal unsafe class KeybindManager : IDisposable
|
||||
{
|
||||
// Direct channel-switch binds (CMD_SAY/PARTY/numbered linkshells/…): switch
|
||||
// the game channel AND mirror it onto the resolved tab so the input pill
|
||||
// shows the real send target (pill-sync, Flo decision 2026-06-15).
|
||||
// shows the real send target (pill-sync).
|
||||
Plugin.Instance.Functions.Chat.SetChannel(channel);
|
||||
// Only mirror onto the tab when the game actually accepted the switch — an
|
||||
// empty linkshell slot leaves the game channel untouched, so the pill must
|
||||
|
||||
@@ -135,7 +135,7 @@ internal sealed class PayloadHandlerInitHostedService(
|
||||
{
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// §6.2 cycle-resolution: both singletons exist by the time HostedServices
|
||||
// Cycle resolution: both singletons exist by the time HostedServices
|
||||
// run, so this is the first safe point to wire the setter.
|
||||
messageList.AttachPayloadHandler(payloadHandler);
|
||||
|
||||
@@ -172,7 +172,7 @@ internal sealed class PayloadHandlerInitHostedService(
|
||||
// InputBar -> CommandHelpWindow -> MainWindow -> InputBar (MS.DI does not catch
|
||||
// it through FactoryCallSite registrations and the resolve recurses silently).
|
||||
// Both singletons exist by host.StartAsync time, so this is the first safe point
|
||||
// to wire the setter — same §6.2 pattern as MessageList.AttachPayloadHandler.
|
||||
// to wire the setter — same setter-injection pattern as MessageList.AttachPayloadHandler.
|
||||
internal sealed class CommandHelpWindowInitHostedService(
|
||||
CommandHelpWindow commandHelpWindow,
|
||||
MainWindow mainWindow
|
||||
@@ -190,7 +190,7 @@ internal sealed class CommandHelpWindowInitHostedService(
|
||||
// Attaches the singleton PayloadHandler to every pre-allocated pop-out
|
||||
// window's MessageList post-container-build. Pool/window cannot take the
|
||||
// PayloadHandler via ctor (that would close the silent FactoryCallSite cycle —
|
||||
// same §6.2 reason as MessageList.AttachPayloadHandler / CommandHelpWindow.
|
||||
// same setter-injection reason as MessageList.AttachPayloadHandler / CommandHelpWindow.
|
||||
// AttachMainWindow). Both singletons exist by host.StartAsync time.
|
||||
internal sealed class ChannelPopoutInitHostedService(
|
||||
ChannelPopoutPool pool,
|
||||
|
||||
@@ -20,7 +20,7 @@ internal sealed class TypingIpc : IDisposable
|
||||
private ICallGateProvider<ChatInputState> StateQueryGate { get; }
|
||||
private ICallGateProvider<ChatInputState, object?> StateChangedGate { get; }
|
||||
|
||||
// v1.4.9 R4: ChatTwo IPC compatibility mirror. Some third-party plugins
|
||||
// v1.4.9: ChatTwo IPC compatibility mirror. Some third-party plugins
|
||||
// have a no-fork policy and subscribe only to ChatTwo.*-prefixed IPC
|
||||
// gates. HellionChat replaces ChatTwo (conflict detection prevents
|
||||
// parallel loading), so mirroring the ChatTwo provider slots lets those
|
||||
@@ -50,7 +50,7 @@ internal sealed class TypingIpc : IDisposable
|
||||
"HellionChat.ChatInputStateChanged"
|
||||
);
|
||||
|
||||
// v1.4.9 R4: ChatTwo-prefixed compatibility slots (see class-level comment).
|
||||
// v1.4.9: ChatTwo-prefixed compatibility slots (see class-level comment).
|
||||
ChatTwoStateQueryGate = Plugin.Interface.GetIpcProvider<ChatInputState>(
|
||||
"ChatTwo.GetChatInputState"
|
||||
);
|
||||
@@ -102,7 +102,7 @@ internal sealed class TypingIpc : IDisposable
|
||||
HasState = true;
|
||||
LastState = state;
|
||||
StateChangedGate.SendMessage(state);
|
||||
// v1.4.9 R4: mirror on ChatTwo-prefixed slot for no-fork-policy plugins.
|
||||
// v1.4.9: mirror on ChatTwo-prefixed slot for no-fork-policy plugins.
|
||||
ChatTwoStateChangedGate.SendMessage(state);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ internal sealed class IpcManager : IDisposable
|
||||
object?
|
||||
> InvokeGate { get; }
|
||||
|
||||
// v1.4.9 R4: ChatTwo IPC compatibility mirror. Third-party plugins with
|
||||
// v1.4.9: ChatTwo IPC compatibility mirror. Third-party plugins with
|
||||
// a no-fork policy (e.g. Artisan, AllaganTools) only subscribe to the
|
||||
// ChatTwo.*-prefixed context-menu integration gates. Mirroring all four
|
||||
// provider slots under the ChatTwo namespace lets those plugins keep
|
||||
@@ -65,7 +65,7 @@ internal sealed class IpcManager : IDisposable
|
||||
object?
|
||||
>("HellionChat.Invoke");
|
||||
|
||||
// v1.4.9 R4: ChatTwo-prefixed mirrors of the four context-menu slots
|
||||
// v1.4.9: ChatTwo-prefixed mirrors of the four context-menu slots
|
||||
// above. Share the same Register/Unregister backing methods so a
|
||||
// plugin that subscribes via either namespace lands in the same
|
||||
// Registered list. SendMessage on Invoke fans out to both gates.
|
||||
@@ -103,7 +103,7 @@ internal sealed class IpcManager : IDisposable
|
||||
)
|
||||
{
|
||||
InvokeGate.SendMessage(id, sender, contentId, payload, senderString, content);
|
||||
// v1.4.9 R4: fan out the same event to plugins listening on ChatTwo.Invoke.
|
||||
// v1.4.9: fan out the same event to plugins listening on ChatTwo.Invoke.
|
||||
ChatTwoInvokeGate.SendMessage(id, sender, contentId, payload, senderString, content);
|
||||
}
|
||||
|
||||
|
||||
@@ -248,9 +248,8 @@ internal class MessageManager : IAsyncDisposable
|
||||
_logger.LogError(ex, "Error in FilterAllTabs");
|
||||
}
|
||||
|
||||
// v1.4.9 R3 profiling: Information so the xllog tail surfaces this
|
||||
// without a Debug filter. Belt-and-suspenders for future plugin-load
|
||||
// regressions; remains in place after Sub-Task 3.4 Befund.
|
||||
// Information, not Debug, so the xllog tail surfaces this without a
|
||||
// filter. Kept as a guard against future plugin-load regressions.
|
||||
_logger.LogInformation($"FilterAllTabs took {stopwatch.ElapsedMilliseconds}ms");
|
||||
});
|
||||
}
|
||||
@@ -436,10 +435,10 @@ internal class MessageManager : IAsyncDisposable
|
||||
// TEST-MIRROR: ../_Helpers/TabSoundDecision.cs
|
||||
// Unseen ("count only what you haven't seen") suppresses unread on an inactive
|
||||
// tab when the active tab ALSO shows this message — you already saw it in the
|
||||
// tab you're looking at (1.5.6 / upstream ChatTwo behavior). Pre-F2 the "active
|
||||
// tab" was wrongly pinned to Tabs[0], so this fired against the wrong tab; F2
|
||||
// recoupled CurrentTab to the REAL active tab, so currentTabMatches is now
|
||||
// measured against the tab you actually see. All -> always counts; None ->
|
||||
// tab you're looking at (1.5.6 / upstream ChatTwo behavior). The "active tab"
|
||||
// used to be pinned to Tabs[0], so this fired against the wrong one until
|
||||
// CurrentTab was recoupled to the real active tab, and currentTabMatches is
|
||||
// now measured against the tab you see. All -> always counts; None ->
|
||||
// counts here and is gated out at the display layer. Pure + SelfTest-able.
|
||||
internal static bool ShouldCountUnread(Tab tab, Tab currentTab, bool currentTabMatches) =>
|
||||
!(
|
||||
|
||||
@@ -245,7 +245,7 @@ internal class MessageStore : IDisposable
|
||||
|
||||
private SqliteConnection Connect()
|
||||
{
|
||||
// v1.4.9 R3 profiling: trace cost of SQLite open + pragma-apply. Paired
|
||||
// v1.4.9 profiling: trace cost of SQLite open + pragma-apply. Paired
|
||||
// with the Migrate-Stopwatch below — Connect alone is the cheap half
|
||||
// (Open + a handful of PRAGMAs); the expensive half typically lives in
|
||||
// Migrate, especially on a large DB after a schema bump.
|
||||
@@ -260,7 +260,7 @@ internal class MessageStore : IDisposable
|
||||
|
||||
private void Migrate()
|
||||
{
|
||||
// v1.4.9 R3 profiling: trace cost of the schema-migration chain. On a
|
||||
// v1.4.9 profiling: trace cost of the schema-migration chain. On a
|
||||
// large DB after a fresh schema bump this is the dominant SQLite cost
|
||||
// at plugin-load, not Connect.
|
||||
var migrateSw = System.Diagnostics.Stopwatch.StartNew();
|
||||
@@ -939,8 +939,7 @@ internal class MessageStore : IDisposable
|
||||
// storage form on both sides so the IN(...) compare matches. SQLite has a
|
||||
// hard parameter limit of 999 in default builds, so we chunk the input --
|
||||
// a 1000-hit FTS query never explodes the SELECT. Result ordering is not
|
||||
// guaranteed; callers re-sort (e.g. DbViewer sorts by Date descending in
|
||||
// Sub-Task 4.4).
|
||||
// guaranteed; callers re-sort (DbViewer sorts by Date descending).
|
||||
public IReadOnlyList<Message> LoadByGuids(IReadOnlyList<string> guidStrings)
|
||||
{
|
||||
if (guidStrings.Count == 0)
|
||||
|
||||
@@ -263,7 +263,7 @@ internal sealed class PayloadHandler
|
||||
// Eureka, Bozja and Occult need special handling as tells work different
|
||||
if (!Sheets.IsInForay())
|
||||
{
|
||||
// §6.9: single SetPendingMessage call; v1.5.6 used incremental Chat += writes.
|
||||
// Single SetPendingMessage call; v1.5.6 used incremental Chat += writes.
|
||||
// XC-8: shares the /tell builder with the native detours. IsPublic (not
|
||||
// IsNullOrEmpty) is resolved HERE — a private/null world must NOT leak @World.
|
||||
_inputBar.SetPendingMessage(
|
||||
@@ -393,7 +393,7 @@ internal sealed class PayloadHandler
|
||||
var inputChannel = chunk.Message?.Code.Type.ToInputChannel();
|
||||
if (inputChannel != null && ImGui.Selectable(Language.Context_ReplyInSelectedChatMode))
|
||||
{
|
||||
// §6.3: route channel-switch through MainWindow's active tab
|
||||
// Route the channel switch through MainWindow's active tab
|
||||
_mainWindow.ActiveTab?.CurrentChannel?.SetChannel(inputChannel.Value);
|
||||
_inputBar.Activate = true;
|
||||
}
|
||||
@@ -731,7 +731,7 @@ internal sealed class PayloadHandler
|
||||
using (ImRaii.Tooltip())
|
||||
using (ImRaii.TextWrapPos(0.0f))
|
||||
using (
|
||||
// §4.2: use active theme text colour instead of the former LogWindow.DefaultText static.
|
||||
// Use the active theme text colour instead of the former LogWindow.DefaultText static.
|
||||
ImRaii.PushColor(
|
||||
ImGuiCol.Text,
|
||||
ColourUtil.RgbaToVector4(_themes.Active.Colors.TextPrimary)
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace HellionChat;
|
||||
|
||||
// Builds the generic-host DI container that drives v1.5.0+. The factory is
|
||||
// invoked synchronously from Plugin.ctor (after the schema gate clears) so the
|
||||
// container exists before PluginLifecycle.LoadAsync runs. See plan §1 for the
|
||||
// container exists before PluginLifecycle.LoadAsync runs. For the
|
||||
// deliberate divergence from Lightless' deferred Func-delegate pattern.
|
||||
internal static class PluginHostFactory
|
||||
{
|
||||
@@ -48,7 +48,7 @@ internal static class PluginHostFactory
|
||||
PluginHostDependencies dependencies
|
||||
)
|
||||
{
|
||||
// Block A — Dalamud services (21 [PluginService] singletons).
|
||||
// Dalamud services (21 [PluginService] singletons).
|
||||
services.AddSingleton(dependencies);
|
||||
services.AddSingleton(dependencies.PluginInterface);
|
||||
services.AddSingleton(dependencies.PluginLog);
|
||||
@@ -77,7 +77,7 @@ internal static class PluginHostFactory
|
||||
services.AddSingleton(plugin.WindowSystem);
|
||||
services.AddSingleton<PluginLifecycle>();
|
||||
|
||||
// Block B — HellionChat singletons. Factory lambdas because most
|
||||
// HellionChat singletons. Factory lambdas because most
|
||||
// classes are internal-sealed and the default activator only sees
|
||||
// public ctors.
|
||||
services.AddSingleton<IPlatformUtil>(_ => new DalamudPlatformUtil());
|
||||
@@ -307,7 +307,7 @@ internal static class PluginHostFactory
|
||||
// Pop-out windows: each gets its OWN MessageList + InputBar so the
|
||||
// channel pill and message scroll are per-window. The PayloadHandler is
|
||||
// attached post-build (ChannelPopoutInitHostedService), NEVER via ctor
|
||||
// (plan §B.2 — would close a silent FactoryCallSite cycle).
|
||||
// (would close a silent FactoryCallSite cycle).
|
||||
services.AddSingleton<Func<int, Ui.Windows.ChannelPopoutWindow>>(sp =>
|
||||
slot => new Ui.Windows.ChannelPopoutWindow(
|
||||
slot,
|
||||
@@ -336,7 +336,7 @@ internal static class PluginHostFactory
|
||||
sp.GetRequiredService<ILogger<Ui.Windows.ChannelPopoutPool>>()
|
||||
));
|
||||
|
||||
// Block C — Windows. WindowSystem.AddWindow is called from
|
||||
// Windows. WindowSystem.AddWindow is called from
|
||||
// PluginLifecycle.LoadAsync on the framework thread.
|
||||
services.AddSingleton(sp => new Ui.Windows.SettingsWindow(
|
||||
sp.GetRequiredService<Plugin>(),
|
||||
@@ -380,8 +380,8 @@ internal static class PluginHostFactory
|
||||
));
|
||||
#endif
|
||||
// The style lab: variants side by side, in-game, against the live theme.
|
||||
// Permanent by Flo's call, and deliberately not behind DEBUG -- style
|
||||
// decisions happen in the build he actually runs.
|
||||
// Permanent, and deliberately not behind DEBUG: style decisions get
|
||||
// made in the build that actually ships.
|
||||
services.AddSingleton(sp => new Ui.Windows.InputBarLabWindow(
|
||||
sp.GetRequiredService<Plugin>()
|
||||
));
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace HellionChat.Privacy;
|
||||
|
||||
internal static class PrivacyDefaults
|
||||
{
|
||||
// F3.1: failsafe for ChatTypes added by future FFXIV patches. New installs
|
||||
// Failsafe for ChatTypes added by future FFXIV patches. New installs
|
||||
// persist unknown channels so a major patch's added ChatType isn't silently
|
||||
// dropped before the user can opt in or out. Existing configs keep their
|
||||
// explicit choice — see Configuration.cs PrivacyPersistUnknownChannels.
|
||||
|
||||
@@ -56,7 +56,7 @@ internal sealed class CardClipPlanStep : ISelfTestStep
|
||||
int remaining;
|
||||
try
|
||||
{
|
||||
// v1.10.0/A1: the fingerprint gate waits for the value to settle, so
|
||||
// v1.10.0: the fingerprint gate waits for the value to settle, so
|
||||
// the step walks a synthetic clock past the window instead of sleeping.
|
||||
var clock = Environment.TickCount64;
|
||||
messages.RunHeightCacheInvalidationForSelfTest(tab, 400f, clock);
|
||||
|
||||
@@ -3,7 +3,7 @@ using Dalamud.Plugin.SelfTest;
|
||||
|
||||
namespace HellionChat.SelfTests;
|
||||
|
||||
// F2: CurrentTab is coupled to MainWindow.ActiveTab (no longer the fixed index-0
|
||||
// CurrentTab is coupled to MainWindow.ActiveTab (no longer the fixed index-0
|
||||
// Tabs lookup). Asserts ReferenceEquals between the two, with false-green
|
||||
// defenses: (1) empty-config exercises the getter's fallback; (2) null ActiveTab
|
||||
// opens the window so the Draw-seed sets it and retries via Waiting (bounded so a
|
||||
|
||||
@@ -5,12 +5,12 @@ using HellionChat.GameFunctions.Types;
|
||||
|
||||
namespace HellionChat.SelfTests;
|
||||
|
||||
// F2 (guided): interactive, fires NO synthetic probes. Shows the full measured
|
||||
// Guided: interactive, fires NO synthetic probes. Shows the full measured
|
||||
// state every frame so a result is observable, not a guess, and walks the user
|
||||
// through the real switch-away-and-back flow. It verifies the PRIVACY-relevant
|
||||
// effect, keyed on the tab type:
|
||||
// - a NORMAL tab carrying a game-side tell must lose its RUNTIME target
|
||||
// (CurrentChannel.TellTarget) on switch-away-and-back (the F1 strip), so a
|
||||
// (CurrentChannel.TellTarget) on switch-away-and-back, so a
|
||||
// typed line can't /tell the old partner;
|
||||
// - a BOUND auto-tell tab keeps its partner by design (leg1) — its binding is
|
||||
// Tab.TellTarget and is deliberately untouched by the strip.
|
||||
|
||||
@@ -7,7 +7,7 @@ using HellionChat.Util;
|
||||
|
||||
namespace HellionChat.SelfTests;
|
||||
|
||||
// v1.12.0/A2: the exporter now reads text from the chunk lists instead of the
|
||||
// v1.12.0: the exporter now reads text from the chunk lists instead of the
|
||||
// raw SeStrings. That change is invisible to the build suite -- ExportToFile
|
||||
// takes IEnumerable<Message>, Message needs SeString, and xUnit cannot load
|
||||
// Dalamud.dll, so even an empty list fails before the body runs.
|
||||
|
||||
@@ -127,7 +127,7 @@ internal sealed class FontManagerCtorSmokeStep : ISelfTestStep
|
||||
return SelfTestStepResult.Fail;
|
||||
}
|
||||
|
||||
// Report what was actually verified (Flo's request: don't just show Pass).
|
||||
// Report what was actually verified rather than a bare Pass.
|
||||
// The glyph-range entry counts make the B1 dedup visible — the cjk-fallback
|
||||
// range is now a small trimmed remainder next to the large primary range.
|
||||
var counts = fm.GlyphRangeLengths;
|
||||
|
||||
@@ -43,7 +43,7 @@ internal sealed class GlobalStyleScopeAllocStep : ISelfTestStep
|
||||
GlobalStyleScope.Push(theme, registry, opacity).Dispose();
|
||||
var delta = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
|
||||
// Report the measured figure on BOTH outcomes (Flo's request: don't just
|
||||
// Report the measured figure on BOTH outcomes (a bare Pass hides
|
||||
// show Pass) — the byte delta is the whole point of the GC-reserve probe.
|
||||
var ok = delta <= AllocBudgetBytes;
|
||||
var status = ok ? "PASS" : "FAIL";
|
||||
|
||||
@@ -68,7 +68,7 @@ internal sealed class HonorificHeaderRenderStep : ISelfTestStep
|
||||
);
|
||||
|
||||
// Draw at a deliberately wide 420px so the title never hits the truncation
|
||||
// clamp — LastTitleRendered then reflects the GATE outcome, not the width.
|
||||
// clamp -- LastTitleRendered then reflects the gate outcome, not the width.
|
||||
try
|
||||
{
|
||||
// (a) available + valid title + toggle on -> title renders
|
||||
|
||||
@@ -4,7 +4,7 @@ using HellionChat.Ui.StyleEngine;
|
||||
|
||||
namespace HellionChat.SelfTests;
|
||||
|
||||
// Master-spec §7.5 scope note: the hover registry must not grow frame by frame.
|
||||
// the hover registry must not grow frame by frame.
|
||||
// Successor to HoverSheenAllocStep, which pinned the same contract against the
|
||||
// old sheen start-timestamp dictionary.
|
||||
//
|
||||
|
||||
@@ -7,8 +7,8 @@ namespace HellionChat.SelfTests;
|
||||
// B1-2 window flags. Drives the REAL MainWindow.PreDraw and asserts it wired
|
||||
// Window.Flags to ResolveFlags(CanMove, CanResize), then pins the pure
|
||||
// fresh-base contract: false/false adds NoMove|NoResize, true/true clears them
|
||||
// (the masterplan's "flags must rebuild from a fresh base, else NoMove sticks
|
||||
// after toggling back" risk). NoScrollbar|NoScrollWithMouse always present.
|
||||
// -- flags must rebuild from a fresh base, or NoMove sticks after toggling
|
||||
// back. NoScrollbar|NoScrollWithMouse always present.
|
||||
// Non-test caller of ResolveFlags: MainWindow.PreDraw.
|
||||
internal sealed class MainWindowFlagsStep : ISelfTestStep
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace HellionChat.SelfTests;
|
||||
// step so the per-frame hot path never references file IO. Writes one
|
||||
// perf-baseline.json into the plugin ConfigDirectory, atomically (tmp + move)
|
||||
// like ThemeRegistry's theme writer, so a mid-write crash leaves either the
|
||||
// old file or the new file, never a half JSON. Field names track §7.5:
|
||||
// old file or the new file, never a half JSON. Field names track the performance-baseline layout:
|
||||
// steady-state Draw cost (avg/max ms), the quad-proxy draw-call count
|
||||
// (avg/max), and frame delta (avg/max). First-frame-HITCH is read off
|
||||
// drawMs max/avg by the human author, platform-annotated in the notes.
|
||||
|
||||
@@ -6,14 +6,14 @@ namespace HellionChat.SelfTests;
|
||||
// Optional metric capture. Accumulates 1000 steady-state frames of ImGui IO
|
||||
// counters plus the plugin's full-Draw wall-time (Plugin.LastDrawMs, B5-1),
|
||||
// then writes a single perf-baseline.json into the plugin ConfigDirectory so
|
||||
// the cycle-notes author can copy the §7.5 figures without a separate
|
||||
// the cycle-notes author can copy the baseline figures without a separate
|
||||
// profiling harness. The step only records — it never fails on a threshold
|
||||
// (the budgets are evaluated by a human against the JSON, §7.5 "optional,
|
||||
// (the budgets are evaluated by a human against the JSON ("optional,
|
||||
// manual"). It returns Waiting until the sample window fills, mirroring the
|
||||
// per-frame poll idiom of ThemeSwitchSelfTestStep.
|
||||
internal sealed class PerformanceBaselineStep : ISelfTestStep
|
||||
{
|
||||
// §7.5 steady-state window. 1000 frames ≈ 16s at 60fps, long enough to
|
||||
// Steady-state window. 1000 frames ≈ 16s at 60fps, long enough to
|
||||
// average out GC blips without making the manual step tedious.
|
||||
private const int TargetFrames = 1000;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ using HellionChat.Code;
|
||||
|
||||
namespace HellionChat.SelfTests;
|
||||
|
||||
// F3: the unread dot the v1.8.x sidebar rebuild dropped. Drives the REAL
|
||||
// The unread dot the v1.8.x sidebar rebuild dropped. Drives the REAL
|
||||
// Sidebar.Draw (render precedent: SidebarGreetedGlyphStep) with a probe tab that
|
||||
// is inactive and carries Unread>0, then reads the render-observability counter
|
||||
// so a regressed/absent dot fails. Asserts: dot drawn for an inactive Unseen tab;
|
||||
|
||||
@@ -7,7 +7,7 @@ using HellionChat.Util;
|
||||
|
||||
namespace HellionChat.SelfTests;
|
||||
|
||||
// F1: the activation strip. Drives the REAL OnTabActivated — the entry the
|
||||
// The activation strip. Drives the REAL OnTabActivated -- the entry the
|
||||
// Sidebar/TopTabBar click handlers, the pop-out path and the Draw-seed all call
|
||||
// — with local probe tabs (Plugin.Config.Tabs is never touched). Asserts the
|
||||
// five contracts: strip-on-switch, no-strip-on-reclick (TR-4), leg1 preserve,
|
||||
|
||||
@@ -7,7 +7,7 @@ using HellionChat.Ui.StyleEngine;
|
||||
|
||||
namespace HellionChat.SelfTests;
|
||||
|
||||
// v1.13.0/A7: the type scale has no call site in the message list until block C,
|
||||
// v1.13.0: the type scale has no call site in the message list until block C,
|
||||
// so without this step block A would end with nothing to look at and two helpers
|
||||
// (TypeScale, BaselineMath) with no caller at all.
|
||||
//
|
||||
|
||||
@@ -3,12 +3,12 @@ using Dalamud.Plugin.SelfTest;
|
||||
|
||||
namespace HellionChat.SelfTests;
|
||||
|
||||
// F3: the unread decision (MessageManager.ShouldCountUnread). Unseen suppresses
|
||||
// The unread decision (MessageManager.ShouldCountUnread). Unseen suppresses
|
||||
// unread on an inactive tab only when the active tab ALSO shows the message (you
|
||||
// saw it there) — 1.5.6/upstream semantics, now measured against the REAL active
|
||||
// tab thanks to F2. Asserts the truth table: suppressed when active tab also
|
||||
// matches; counts when it does not (the Carla/Jin case); All always counts; None
|
||||
// counts at the increment layer (the display gate hides it).
|
||||
// saw it there) -- 1.5.6/upstream semantics, measured against the real active
|
||||
// tab. Asserts the truth table: suppressed when the active tab also matches;
|
||||
// counts when it does not; All always counts; None counts at the increment
|
||||
// layer (the display gate hides it).
|
||||
internal sealed class UnreadDecisionStep : ISelfTestStep
|
||||
{
|
||||
public string Name => "Hellion Chat - Unread decision (per active tab)";
|
||||
@@ -27,7 +27,8 @@ internal sealed class UnreadDecisionStep : ISelfTestStep
|
||||
}
|
||||
|
||||
// (b) inactive Unseen tab + the active tab does NOT show the message
|
||||
// (currentTabMatches=false) => counts (badge). The Carla/Jin case.
|
||||
// (currentTabMatches=false) => counts (badge). Two people talking in
|
||||
// a channel the active tab does not carry.
|
||||
if (!MessageManager.ShouldCountUnread(inactive, active, currentTabMatches: false))
|
||||
{
|
||||
ImGui.Text("(b) inactive Unseen tab must count when the active tab does not show it");
|
||||
|
||||
@@ -64,7 +64,7 @@ internal sealed class WizardStateSmokeStep : ISelfTestStep
|
||||
// jumps straight to Step 4 (no Step-3 entry → no seed for
|
||||
// FilterIncludePreviousSessions), commits, and asserts the history
|
||||
// toggle remained on its pre-test value. Pins the null-semantics
|
||||
// from Spec Z.176 so a regression in CommitPending that started
|
||||
// so a regression in CommitPending that started
|
||||
// writing seeded recommendations unconditionally would surface
|
||||
// here.
|
||||
// CommitPending → ApplyRoleplay overwrites six privacy /
|
||||
|
||||
@@ -5,7 +5,7 @@ using Microsoft.Extensions.Logging;
|
||||
namespace HellionChat.Services;
|
||||
|
||||
// Routes an incoming tell to the configured TellAutoOpenMode (Off/Sidebar/
|
||||
// TopTab/Popout). Decoupled from AutoTellTabsService (Flo decision 2026-06-15):
|
||||
// TopTab/Popout). Deliberately decoupled from AutoTellTabsService:
|
||||
// that service owns tab CREATION + lifecycle; this only REVEALS/pops the tab it
|
||||
// finds. Popout guards on pool.IsOpen so it never double-pops a tab the
|
||||
// AutoTellTabsOpenAsPopout path already opened. Subscribes to the resolved
|
||||
|
||||
@@ -54,7 +54,7 @@ public sealed class ThemeRegistry
|
||||
internal void SetActiveChangedCallback(Action callback) => _onActiveChanged = callback;
|
||||
|
||||
// Shared slug guard for any code path that turns a slug into a filename.
|
||||
// Both SaveEditingBuffer (F1) and ImportFromPath (M6) call this so the
|
||||
// Both SaveEditingBuffer and ImportFromPath call this so the
|
||||
// path-traversal/invalid-char rules live in exactly one place.
|
||||
//
|
||||
// Whitespace rejection is intentional: Path.GetInvalidFileNameChars on
|
||||
@@ -112,7 +112,7 @@ public sealed class ThemeRegistry
|
||||
public Theme Active => _active;
|
||||
|
||||
// Read-only exposure of the configured custom themes directory.
|
||||
// M6 ThemeImportExportRow opens this path via Process.Start.
|
||||
// ThemeImportExportRow opens this path via Process.Start.
|
||||
public string? CustomThemesDir => _customThemesDir;
|
||||
|
||||
// Read-only enumeration of all built-in theme slugs. T2 ThemePickerCategoryStep
|
||||
@@ -120,8 +120,8 @@ public sealed class ThemeRegistry
|
||||
public IEnumerable<string> BuiltinSlugs => _builtIns.Keys;
|
||||
|
||||
// True try-pattern lookup: returns false when neither built-in nor custom
|
||||
// cache holds the slug, no fallback to default. M3 ThemePicker uses this
|
||||
// for card-rendering, M6 ThemeImportExportRow for fork-slug collisions.
|
||||
// cache holds the slug, no fallback to default. ThemePicker uses this
|
||||
// for card-rendering, ThemeImportExportRow for fork-slug collisions.
|
||||
// Cold-cache fallback: see `LoadCustomBySlug` lookup-by-slug reverse
|
||||
// iteration — it only walks the pre-populated _customCache. If a freshly
|
||||
// imported file has not been enumerated yet (or no warm-up ran), the first
|
||||
@@ -292,7 +292,7 @@ public sealed class ThemeRegistry
|
||||
// Switch() prefers built-ins over custom themes with the same slug
|
||||
// (see `Switch` built-in-first lookup), so saving a custom file under
|
||||
// a built-in slug persists the file but leaves the built-in active —
|
||||
// looks green, behaves broken. M4 ColorPicker DrawIdleState forks
|
||||
// looks green, behaves broken. ColorPicker DrawIdleState forks
|
||||
// built-in themes into a custom slug before BeginEditing, M6
|
||||
// ImportFromPath renames built-in-colliding imports to <slug>_imported.
|
||||
// New call-sites must either fork first or rename to a non-built-in slug.
|
||||
@@ -308,7 +308,7 @@ public sealed class ThemeRegistry
|
||||
// separators, parent-directory tokens, or platform-invalid filename chars.
|
||||
// Without this guard an imported theme with Slug "../../../etc/passwd"
|
||||
// would let Path.Combine escape _customThemesDir entirely. Shared helper
|
||||
// so M6 ImportFromPath uses the exact same rule set.
|
||||
// so ImportFromPath uses the exact same rule set.
|
||||
var safeSlug = _editingThemeBuffer.Slug;
|
||||
if (!IsSafeThemeSlug(safeSlug))
|
||||
{
|
||||
@@ -326,8 +326,8 @@ public sealed class ThemeRegistry
|
||||
// persist a custom file under a built-in slug — the file lands on disk,
|
||||
// Switch keeps the built-in active, and the post-save active-slug check
|
||||
// below returns false. The caller then sees "save failed" while a garbage
|
||||
// file accumulates in the themes dir on every retry. M4 ColorPicker forks
|
||||
// built-in themes into a custom slug before BeginEditing, M6 ImportFromPath
|
||||
// file accumulates in the themes dir on every retry. ColorPicker forks
|
||||
// built-in themes into a custom slug before BeginEditing, ImportFromPath
|
||||
// renames built-in-colliding imports to <slug>_imported, so production
|
||||
// paths already steer clear; this guard catches everything else.
|
||||
if (_builtIns.ContainsKey(safeSlug))
|
||||
@@ -522,8 +522,8 @@ public sealed class ThemeRegistry
|
||||
)
|
||||
{
|
||||
var t = (float)(now - _crossfadeStartTickMs) / CrossfadeDurationMs;
|
||||
// A2: SmoothStep easing so the fade eases in/out instead of a
|
||||
// linear ramp. MUST stay in lockstep with TryGetActiveCrossfade (K8).
|
||||
// SmoothStep easing so the fade eases in and out instead of running
|
||||
// linear. MUST stay in lockstep with TryGetActiveCrossfade.
|
||||
var te = t * t * (3f - 2f * t);
|
||||
snapshot = ThemeAbgrCacheLerp.Lerp(_previousAbgrSnapshot.Value, _active.AbgrCache, te);
|
||||
}
|
||||
@@ -551,7 +551,7 @@ public sealed class ThemeRegistry
|
||||
return false;
|
||||
|
||||
var t = (float)elapsed / CrossfadeDurationMs;
|
||||
// A2: SmoothStep easing -- keep identical to ArmCrossfade (K8).
|
||||
// SmoothStep easing -- keep identical to ArmCrossfade.
|
||||
var te = t * t * (3f - 2f * t);
|
||||
lerped = ThemeAbgrCacheLerp.Lerp(_previousAbgrSnapshot.Value, _active.AbgrCache, te);
|
||||
return true;
|
||||
|
||||
@@ -18,7 +18,7 @@ internal sealed class CommandHelpWindow : Window
|
||||
// Setter-injected post-ctor to break the InputBar -> CommandHelpWindow ->
|
||||
// MainWindow -> InputBar singleton cycle (MS.DI does not detect cycles
|
||||
// through FactoryCallSite registrations). Wired in
|
||||
// CommandHelpWindowInitHostedService.StartAsync, same §6.2 pattern as
|
||||
// CommandHelpWindowInitHostedService.StartAsync, same setter-injection pattern as
|
||||
// MessageList.AttachPayloadHandler.
|
||||
private Windows.MainWindow? _mainWindow;
|
||||
|
||||
@@ -41,7 +41,7 @@ internal sealed class CommandHelpWindow : Window
|
||||
RespectCloseHotkey = false;
|
||||
DisableWindowSounds = true;
|
||||
|
||||
// Logger injected for future diagnostic hooks (no call-sites yet in R2).
|
||||
// Logger injected for future diagnostic hooks; no call sites yet.
|
||||
_ = _logger;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
namespace HellionChat.Ui.Components;
|
||||
|
||||
// B2 (PERF-B2): variable-height clip plan. ImGuiListClipper needs a constant
|
||||
// row height, and since v1.10.0/A2 neither density has one (compact rows wrap
|
||||
// row height, and since v1.10.0 neither density has one (compact rows wrap
|
||||
// too), so both compute a plan from the cached per-row heights: a lead dummy
|
||||
// for the rows above
|
||||
// the viewport, the [first..last] index range that overlaps the viewport, and
|
||||
|
||||
@@ -118,7 +118,7 @@ internal sealed class InputBar
|
||||
// Note: when MainWindow is closed, DrawInputField never runs, so
|
||||
// _isFocused keeps the last value written by the previous draw pass.
|
||||
// The consumer that actually pushes this state across the IPC boundary
|
||||
// (TypingIpc.BuildState, see F3 Step 2) gates on Plugin.MainWindow.IsOpen
|
||||
// (TypingIpc.BuildState) gates on Plugin.MainWindow.IsOpen
|
||||
// itself, so the stale backing-field never leaks to subscribers. Mirroring
|
||||
// the gate here would require an extra Plugin-backref in InputBar that the
|
||||
// rest of the component doesn't need.
|
||||
@@ -1183,7 +1183,7 @@ internal sealed class InputBar
|
||||
}
|
||||
|
||||
// DTO for an in-flight auto-translate completion. Lives as a companion type
|
||||
// in this file because it is only consumed by InputBar (see v1.7.1 Fix #4 plan §2.4).
|
||||
// in this file because it is only consumed by InputBar.
|
||||
internal sealed class AutoCompleteInfo
|
||||
{
|
||||
// ToComplete MUST be a mutable field (not an auto-property), because the
|
||||
|
||||
@@ -38,7 +38,7 @@ internal sealed class MessageList
|
||||
private readonly Action<Message, string?> _drawCardRow;
|
||||
|
||||
// Reused across frames: at MessageManager.MessageDisplayLimit a fresh array
|
||||
// per frame is 40 KB of garbage, and A2 put the default density on this
|
||||
// per frame is 40 KB of garbage, and A later cycle 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 = [];
|
||||
@@ -49,7 +49,7 @@ internal sealed class MessageList
|
||||
private bool _stampVisible;
|
||||
private float _metaDrop;
|
||||
|
||||
// §6.2: setter-injection breaks the PayloadHandler → MainWindow → MessageList → PayloadHandler 3-cycle.
|
||||
// Setter-injection breaks the PayloadHandler → MainWindow → MessageList → PayloadHandler 3-cycle.
|
||||
// Wired by PayloadHandlerInitHostedService.StartAsync after both singletons exist.
|
||||
internal void AttachPayloadHandler(PayloadHandler handler)
|
||||
{
|
||||
@@ -88,7 +88,7 @@ internal sealed class MessageList
|
||||
|
||||
// SelfTest hook (B2): drives the live invalidation, returns the tab's remaining
|
||||
// cached-height count so the step can assert the drop. nowMs is a parameter so
|
||||
// the step can step past the settle window without sleeping (v1.10.0/A1).
|
||||
// the step can step past the settle window without sleeping (v1.10.0).
|
||||
internal int RunHeightCacheInvalidationForSelfTest(Tab tab, float contentWidth, long nowMs)
|
||||
{
|
||||
InvalidateHeightCacheIfLayoutChanged(tab, contentWidth, nowMs);
|
||||
@@ -569,7 +569,7 @@ internal sealed class MessageList
|
||||
// B2-1/B2-2: sender via DrawChunks (name-aware path), on its own line
|
||||
// with content below — 1.5.6 card parity (ChatLogWindow.cs:1913, no
|
||||
// SameLine after the sender). The 1.5.6 channel-colour push on the
|
||||
// sender is deferred styling polish (masterplan §6 -> v1.9.0); plain
|
||||
// sender is deferred styling polish (deferred to v1.9.0); plain
|
||||
// text here.
|
||||
// A system message has no sender, so a header row would be a stamp on a
|
||||
// line of its own -- an empty gesture. Those stay single-line in both
|
||||
|
||||
@@ -20,7 +20,7 @@ internal sealed class LivePreviewPanel : IDisposable
|
||||
private static uint Abgr(StyleEngine.Token token, ThemeColors colors) =>
|
||||
ColourUtil.RgbaToAbgr(Tokens.Resolve(token, colors));
|
||||
|
||||
// Static counter for S5 reload-stress verification: after 10 reloads the
|
||||
// Static counter for reload-stress verification: after 10 reloads the
|
||||
// counter must read 0 (plugin disabled) or 1 (plugin enabled). Anything
|
||||
// higher signals a Dispose skip and a subscriber leak against ThemeRegistry.
|
||||
internal static int InstanceCount;
|
||||
|
||||
@@ -254,7 +254,7 @@ internal sealed class DataPrivacyTab
|
||||
)
|
||||
{
|
||||
// Read-only statement, not a switch. Do not promote it to one
|
||||
// without an explicit Sub-Spec change: a toggle implies there is
|
||||
// without an explicit design change: a toggle implies there is
|
||||
// something to turn off.
|
||||
ImGuiUtil.HelpText(HellionStrings.Settings_Telemetry_None);
|
||||
}
|
||||
|
||||
@@ -173,8 +173,7 @@ internal sealed class ThemeImportExportRow
|
||||
// Slug sanitisation BEFORE BeginEditing — SaveEditingBuffer would
|
||||
// reject too, but rejecting here means an unsafe slug never enters
|
||||
// the editing buffer. Shared helper ThemeRegistry.IsSafeThemeSlug
|
||||
// keeps the rule set in sync with F1's save-side guard (see
|
||||
// ThemeRegistry.IsSafeThemeSlug shared helper).
|
||||
// keeps the rule set in sync with the save-side guard.
|
||||
var importSlug = theme.Slug;
|
||||
if (!ThemeRegistry.IsSafeThemeSlug(importSlug))
|
||||
{
|
||||
@@ -186,9 +185,9 @@ internal sealed class ThemeImportExportRow
|
||||
return;
|
||||
}
|
||||
|
||||
// Pragmatic deviation from §1.6 wording ("File.Copy into themes/"):
|
||||
// BeginEditing+SaveEditingBuffer produces the same end-state and
|
||||
// reuses the validated F1 save pipeline. Trade-off: destination
|
||||
// Not a plain File.Copy into themes/: BeginEditing+SaveEditingBuffer
|
||||
// produces the same end state and reuses the validated save
|
||||
// pipeline. Trade-off: destination
|
||||
// filename becomes the theme's slug, not the original filename.
|
||||
//
|
||||
// Slug-collision handling:
|
||||
|
||||
@@ -54,7 +54,7 @@ internal sealed class InputPreview : Window
|
||||
DisableWindowSounds = true;
|
||||
IsOpen = true;
|
||||
|
||||
// TODO Polish-Sweep: remove discard once logging call-sites exist
|
||||
// TODO: remove discard once logging call sites exist
|
||||
_ = _logger;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace HellionChat.Ui.StyleEngine;
|
||||
// ABGR before delegating to ImDrawList.
|
||||
internal static class DrawListExtensions
|
||||
{
|
||||
// A1 accent-tint (Variante A): how far the white sweep is pulled toward
|
||||
// Accent tint: how far the white sweep is pulled toward
|
||||
// the element's accent hue. Kept low so the sheen reads as a tinted
|
||||
// highlight, not a saturated accent flash (effect level "subtle").
|
||||
private const float SheenTintStrength = 0.35f;
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace HellionChat.Ui.StyleEngine;
|
||||
// compare an unscaled value (Sidebar.GetWidth, the width slider bounds).
|
||||
//
|
||||
// Deliberately NOT part of ThemeLayout: that record is serialised into theme
|
||||
// JSON, and layout customisation is out of scope per the master spec.
|
||||
// JSON, and layout customisation is deliberately out of scope.
|
||||
internal static class Metrics
|
||||
{
|
||||
// --- Sidebar ---
|
||||
@@ -65,7 +65,7 @@ internal static class Metrics
|
||||
return _cachedScale;
|
||||
|
||||
// Safe variant: GlobalScale throws while the interface manager is
|
||||
// still coming up, and Block F pulls Metrics into more call sites.
|
||||
// still coming up, and Metrics reaches more call sites than it did.
|
||||
_cachedScale = ImGuiHelpers.GlobalScaleSafe;
|
||||
_cachedFrame = frame;
|
||||
return _cachedScale;
|
||||
|
||||
@@ -23,7 +23,7 @@ internal enum TypeRole
|
||||
// exactly Body and never be pushed -- a value with no call site, which is the one
|
||||
// thing this whole style track exists to stop.
|
||||
//
|
||||
// The factors are defaults, not constants. The master spec puts typography under
|
||||
// The factors are defaults, not constants. Typography is meant to sit under
|
||||
// theme control rather than user control, and ThemeTypography already exists as
|
||||
// the extension point for exactly that. A const would wall it off. What is
|
||||
// deliberately absent either way is a user-facing slider per role.
|
||||
|
||||
@@ -18,8 +18,8 @@ namespace HellionChat.Ui.StyleEngine.Widgets;
|
||||
//
|
||||
// So: no plate, but a tint. A tenth-opacity accent wash falling from the top
|
||||
// edge -- colour as atmosphere rather than as a box, which is what the plate got
|
||||
// wrong. Flo picked it from the lab over the rule-only variant; at this strength
|
||||
// it survives the violet themes that killed the filled bar.
|
||||
// wrong. Picked from the lab over the rule-only variant; at this strength it
|
||||
// survives the violet themes that killed the filled bar.
|
||||
//
|
||||
// Which face draws what is not a style choice here, it is a constraint. The meta
|
||||
// face has a glyph range of ASCII plus a middle dot, so only the world name and
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace HellionChat.Ui.Windows;
|
||||
// windows via the injected factory, all registered once in the WindowSystem
|
||||
// (PluginLifecycle.RegisterWindows, framework thread). Open/Close is IsOpen +
|
||||
// Bind/Unbind only — NEVER runtime AddWindow/RemoveWindow (v1.4.9 Stage-2
|
||||
// freeze lesson). Pure DI-sink: no PayloadHandler in the ctor (plan §B.2).
|
||||
// freeze lesson). Pure DI-sink: no PayloadHandler in the ctor.
|
||||
internal sealed class ChannelPopoutPool
|
||||
{
|
||||
private readonly List<ChannelPopoutWindow> _instances;
|
||||
@@ -30,7 +30,7 @@ internal sealed class ChannelPopoutPool
|
||||
|
||||
// Route each window's in-body close through the pool so closing releases
|
||||
// the slot. Wired here (post-construction) rather than via ctor to avoid
|
||||
// a Window->Pool edge that would re-enter pool resolution (plan §B.2).
|
||||
// a Window->Pool edge that would re-enter pool resolution.
|
||||
foreach (var window in _instances)
|
||||
window.CloseRequested = TryClose;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace HellionChat.Ui.Windows;
|
||||
|
||||
// One pre-allocated pop-out window bound to a single Tab. Pure DI-sink: the
|
||||
// PayloadHandler arrives via AttachPayloadHandler (post-build setter), NEVER
|
||||
// via ctor — see plan §B.2. The ###id carries the slot index so all N
|
||||
// via ctor. The ###id carries the slot index so all N
|
||||
// instances are unique for WindowSystem.AddWindow and ImGui state is stable
|
||||
// per slot (not per bound tab).
|
||||
internal sealed class ChannelPopoutWindow : Window, IFocusableChatWindow
|
||||
@@ -66,10 +66,10 @@ internal sealed class ChannelPopoutWindow : Window, IFocusableChatWindow
|
||||
|
||||
// Wired post-build by ChannelPopoutPool so closing routes through the pool
|
||||
// (which owns the slot map). The window can't reach the pool by ctor without
|
||||
// a DI cycle, so the pool sets this after construction. See plan §B.2.
|
||||
// a DI cycle, so the pool sets this after construction.
|
||||
public Action<Guid>? CloseRequested { get; set; }
|
||||
|
||||
// Post-build setter — see plan §B.2. Wired by ChannelPopoutInitHostedService.
|
||||
// Post-build setter. Wired by ChannelPopoutInitHostedService.
|
||||
public void AttachPayloadHandler(PayloadHandler handler) =>
|
||||
_messages.AttachPayloadHandler(handler);
|
||||
|
||||
@@ -78,7 +78,7 @@ internal sealed class ChannelPopoutWindow : Window, IFocusableChatWindow
|
||||
Bound = tab;
|
||||
|
||||
var isTell = tab is { IsTempTab: true, TellTarget: { } target } && target.IsSet();
|
||||
// Master §4.3 default sizes: Tell is the more compact conversation window.
|
||||
// Default sizes: Tell is the more compact conversation window.
|
||||
Size = isTell ? new Vector2(380f, 320f) : new Vector2(420f, 320f);
|
||||
SizeCondition = ImGuiCond.FirstUseEver;
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ namespace HellionChat.Ui.Windows;
|
||||
// Variants of UI elements, drawn side by side so decisions are made by looking
|
||||
// rather than by imagining. Reachable with /hellion lab.
|
||||
//
|
||||
// Permanent, by Flo's call: a dev playground for seeing ideas in-game against
|
||||
// the live theme. The radios default to whatever shipped, so the window also
|
||||
// Permanent: a playground for seeing ideas in-game against the live theme.
|
||||
// The radios default to whatever shipped, so the window also
|
||||
// documents which variant won and what it beat.
|
||||
//
|
||||
// It exists because the alternative was drawing mockups, and mockups are what
|
||||
|
||||
@@ -62,9 +62,9 @@ internal static class AutoTranslate
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
AllEntries();
|
||||
// v1.4.9 R3 profiling: Information so the xllog tail surfaces this
|
||||
// v1.4.9 profiling: Information so the xllog tail surfaces this
|
||||
// without a Debug filter. Belt-and-suspenders for future plugin-load
|
||||
// regressions; remains in place after Sub-Task 3.4 Befund.
|
||||
// regressions.
|
||||
Plugin.LogProxy.Information(
|
||||
$"Warming up auto-translate took {sw.ElapsedMilliseconds}ms"
|
||||
);
|
||||
|
||||
@@ -9,244 +9,6 @@ namespace HellionChat.Util;
|
||||
|
||||
internal static class ChunkUtil
|
||||
{
|
||||
// internal static IEnumerable<Chunk> ToChunks(ReadOnlySeString msg, ChunkSource source, ChatType? defaultColour)
|
||||
// {
|
||||
// var chunks = new List<Chunk>();
|
||||
//
|
||||
// var italic = false;
|
||||
// var foreground = new Stack<uint>();
|
||||
// var glow = new Stack<uint>();
|
||||
// Payload? link = null;
|
||||
//
|
||||
// void Append(string text)
|
||||
// {
|
||||
// chunks.Add(new TextChunk(source, link, text)
|
||||
// {
|
||||
// FallbackColour = defaultColour,
|
||||
// Foreground = foreground.Count > 0 ? foreground.Peek() : null,
|
||||
// Glow = glow.Count > 0 ? glow.Peek() : null,
|
||||
// Italic = italic,
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// foreach (var payload in msg)
|
||||
// {
|
||||
// if (payload.Type == ReadOnlySePayloadType.Text)
|
||||
// {
|
||||
// // We don't want to parse any null string
|
||||
// var str = payload.ToString();
|
||||
// var nulIndex = str.IndexOf('\0');
|
||||
// if (nulIndex > 0)
|
||||
// str = str[..nulIndex];
|
||||
// if (string.IsNullOrEmpty(str))
|
||||
// continue;
|
||||
//
|
||||
// Append(str);
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// switch (payload.MacroCode)
|
||||
// {
|
||||
// case MacroCode.Italic:
|
||||
// var newStatus = payload.TryGetExpression(out var expression) && expression.TryGetUInt(out var value) && value == 1;
|
||||
// italic = newStatus;
|
||||
// break;
|
||||
// case MacroCode.Color:
|
||||
// if (payload.TryGetExpression(out var eColor))
|
||||
// {
|
||||
// if (eColor.TryGetPlaceholderExpression(out var ph) && ph == (int)ExpressionType.StackColor)
|
||||
// {
|
||||
// if (foreground.Count > 0)
|
||||
// foreground.Pop();
|
||||
// }
|
||||
// else if (TryResolveUInt(eColor, out var eColorVal))
|
||||
// {
|
||||
// var color = ColourUtil.ArgbToRgba(eColorVal);
|
||||
//
|
||||
// if (color > 0)
|
||||
// foreground.Push(color);
|
||||
// else if (foreground.Count > 0) // Push the previous color as we don't want invisible text
|
||||
// foreground.Push(foreground.Peek());
|
||||
// }
|
||||
// }
|
||||
// break;
|
||||
// case MacroCode.EdgeColor:
|
||||
// if (payload.TryGetExpression(out eColor))
|
||||
// {
|
||||
// if (eColor.TryGetPlaceholderExpression(out var ph) && ph == (int)ExpressionType.StackColor)
|
||||
// {
|
||||
// if (glow.Count > 0)
|
||||
// glow.Pop();
|
||||
// }
|
||||
// else if (TryResolveUInt(eColor, out var eColorVal))
|
||||
// {
|
||||
// glow.Push(ColourUtil.ArgbToRgba(eColorVal));
|
||||
// }
|
||||
// }
|
||||
// break;
|
||||
// case MacroCode.ColorType:
|
||||
// if (!payload.TryGetExpression(out var eColorType) || !eColorType.TryGetUInt(out var eColorTypeVal))
|
||||
// {
|
||||
// if (foreground.Count > 0)
|
||||
// foreground.Pop();
|
||||
// break;
|
||||
// }
|
||||
//
|
||||
// if (eColorTypeVal == 0)
|
||||
// {
|
||||
// if (foreground.Count > 0)
|
||||
// foreground.Pop();
|
||||
// }
|
||||
// else if (Sheets.UIColorSheet.TryGetRow(eColorTypeVal, out var row))
|
||||
// {
|
||||
// foreground.Push(row.Dark);
|
||||
// }
|
||||
// break;
|
||||
// case MacroCode.EdgeColorType:
|
||||
// if (!payload.TryGetExpression(out var eEdgeColor) || !eEdgeColor.TryGetUInt(out var eEdgeColorVal))
|
||||
// {
|
||||
// if (glow.Count > 0)
|
||||
// glow.Pop();
|
||||
// break;
|
||||
// }
|
||||
//
|
||||
// if (eEdgeColorVal == 0)
|
||||
// {
|
||||
// if (glow.Count > 0)
|
||||
// glow.Pop();
|
||||
// }
|
||||
// else if (Sheets.UIColorSheet.TryGetRow(eEdgeColorVal, out var row))
|
||||
// {
|
||||
// glow.Push(row.Dark);
|
||||
// }
|
||||
// break;
|
||||
// case MacroCode.Fixed:
|
||||
// if (!payload.TryGetExpression(out var expr1, out var expr2))
|
||||
// break;
|
||||
//
|
||||
// if (expr1.TryGetUInt(out var group) && expr2.TryGetUInt(out var key))
|
||||
// {
|
||||
// chunks.Add(new IconChunk(source, null, BitmapFontIcon.AutoTranslateBegin));
|
||||
// using var rssb = new RentedSeStringBuilder();
|
||||
// var translatePayload = rssb.Builder
|
||||
// .BeginMacro(MacroCode.Fixed)
|
||||
// .AppendUIntExpression(group - 1)
|
||||
// .AppendUIntExpression(key)
|
||||
// .EndMacro()
|
||||
// .ToReadOnlySeString();
|
||||
//
|
||||
// Append(Plugin.Evaluator.Evaluate(translatePayload).ToString());
|
||||
// chunks.Add(new IconChunk(source, null, BitmapFontIcon.AutoTranslateEnd));
|
||||
// }
|
||||
// break;
|
||||
// case MacroCode.Icon:
|
||||
// if (payload.TryGetExpression(out var eIcon) && TryResolveInt(eIcon, out var iconVal))
|
||||
// chunks.Add(new IconChunk(source, link, (BitmapFontIcon)iconVal));
|
||||
// break;
|
||||
// case MacroCode.Link:
|
||||
// if (!payload.TryGetExpression(
|
||||
// out var linkTypeExpr1,
|
||||
// out var uintExpr2,
|
||||
// out var intExpr3,
|
||||
// out var intExpr4,
|
||||
// out var strExpr5))
|
||||
// break;
|
||||
//
|
||||
// if (!linkTypeExpr1.TryGetUInt(out var linkType))
|
||||
// break;
|
||||
//
|
||||
// switch ((LinkMacroPayloadType)linkType)
|
||||
// {
|
||||
// case LinkMacroPayloadType.Terminator:
|
||||
// link = null;
|
||||
// break;
|
||||
// case LinkMacroPayloadType.MapPosition:
|
||||
// if (!uintExpr2.TryGetUInt(out var ids))
|
||||
// break;
|
||||
//
|
||||
// if (!intExpr3.TryGetInt(out var rawX))
|
||||
// break;
|
||||
//
|
||||
// if (!intExpr4.TryGetInt(out var rawY))
|
||||
// break;
|
||||
//
|
||||
// var mapId = ids & 0xFF;
|
||||
// var territoryId = (ids >> 16) & 0xFF;
|
||||
// break;
|
||||
// case (LinkMacroPayloadType)Payload.EmbeddedInfoType.DalamudLink - 1:
|
||||
// if (!uintExpr2.TryGetUInt(out var commandId))
|
||||
// break;
|
||||
//
|
||||
// if (!intExpr3.TryGetInt(out var extra1))
|
||||
// break;
|
||||
//
|
||||
// if (!intExpr4.TryGetInt(out var extra2))
|
||||
// break;
|
||||
//
|
||||
// if (!strExpr5.TryGetString(out var extraStr))
|
||||
// break;
|
||||
// break;
|
||||
// case LinkMacroPayloadType.Quest:
|
||||
// if (!uintExpr2.TryGetUInt(out var questId))
|
||||
// break;
|
||||
// break;
|
||||
// case LinkMacroPayloadType.Status:
|
||||
// if (!uintExpr2.TryGetUInt(out var statusId))
|
||||
// break;
|
||||
// break;
|
||||
// case LinkMacroPayloadType.Item:
|
||||
// if (!uintExpr2.TryGetUInt(out var itemId))
|
||||
// break;
|
||||
// break;
|
||||
// case LinkMacroPayloadType.Character:
|
||||
// if (!uintExpr2.TryGetUInt(out var flags))
|
||||
// break;
|
||||
//
|
||||
// if (!intExpr3.TryGetUInt(out var worldId))
|
||||
// break;
|
||||
// break;
|
||||
// case LinkMacroPayloadType.PartyFinder:
|
||||
// if (!uintExpr2.TryGetUInt(out var listingId))
|
||||
// break;
|
||||
//
|
||||
// // intExpr3 is unused
|
||||
//
|
||||
// if (!intExpr4.TryGetUInt(out worldId))
|
||||
// break;
|
||||
// break;
|
||||
// case LinkMacroPayloadType.PartyFinderNotification:
|
||||
// // no expr used
|
||||
// break;
|
||||
// case LinkMacroPayloadType.Achievement:
|
||||
// if (!uintExpr2.TryGetUInt(out var achievementId))
|
||||
// break;
|
||||
// break;
|
||||
// }
|
||||
// break;
|
||||
// case MacroCode.NonBreakingSpace:
|
||||
// Append(" ");
|
||||
// break;
|
||||
// case PayloadType.Unknown:
|
||||
// var rawPayload = (RawPayload)payload;
|
||||
// else if (rawPayload.Data.Length > 1 && rawPayload.Data[1] == 0x14)
|
||||
// {
|
||||
// if (glow.Count > 0)
|
||||
// {
|
||||
// glow.Pop();
|
||||
// }
|
||||
// else if (rawPayload.Data.Length > 6 && rawPayload.Data[2] == 0x05 && rawPayload.Data[3] == 0xF6)
|
||||
// {
|
||||
// var (r, g, b) = (rawPayload.Data[4], rawPayload.Data[5], rawPayload.Data[6]);
|
||||
// glow.Push(ColourUtil.ComponentsToRgba(r, g, b));
|
||||
// }
|
||||
// }
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return chunks;
|
||||
// }
|
||||
|
||||
internal static IEnumerable<Chunk> ToChunks(
|
||||
SeString msg,
|
||||
ChunkSource source,
|
||||
@@ -499,47 +261,4 @@ internal static class ChunkUtil
|
||||
|
||||
return BitConverter.ToUInt32(numArray, 0);
|
||||
}
|
||||
|
||||
// private static bool TryResolveUInt(in ReadOnlySeExpressionSpan expression, out uint value)
|
||||
// {
|
||||
// if (expression.TryGetUInt(out value))
|
||||
// return true;
|
||||
//
|
||||
// if (expression.TryGetParameterExpression(out var exprType, out var operand1))
|
||||
// {
|
||||
// if (!TryResolveUInt(operand1, out var paramIndex))
|
||||
// return false;
|
||||
//
|
||||
// if (paramIndex == 0)
|
||||
// return false;
|
||||
//
|
||||
// paramIndex--;
|
||||
// if ((ExpressionType)exprType == ExpressionType.GlobalNumber)
|
||||
// {
|
||||
// value = (uint) GlobalParametersCache.GetValue((int)paramIndex);
|
||||
// return true;
|
||||
// }
|
||||
// // return (ExpressionType)exprType switch
|
||||
// // {
|
||||
// // // ExpressionType.LocalNumber => context.TryGetLNum((int)paramIndex, out value), // lnum
|
||||
// // ExpressionType.GlobalNumber => (uint) GlobalParametersCache.GetValue((int)paramIndex), // gnum
|
||||
// // _ => false, // gstr, lstr
|
||||
// // };
|
||||
// }
|
||||
//
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// [MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
// private static bool TryResolveInt(in ReadOnlySeExpressionSpan expression, out int value)
|
||||
// {
|
||||
// if (TryResolveUInt(expression, out var u32))
|
||||
// {
|
||||
// value = (int)u32;
|
||||
// return true;
|
||||
// }
|
||||
//
|
||||
// value = 0;
|
||||
// return false;
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -11,8 +11,7 @@ namespace HellionChat.Util;
|
||||
// a later settings change reformats history too.
|
||||
// Known trade-off: at non-default settings, this allocates one List<Chunk> per
|
||||
// visible message per frame. Lists are small and the path is skipped at the
|
||||
// neutral defaults, so GC pressure is low in practice. Accepted; noted in
|
||||
// Cycle Notes.
|
||||
// neutral defaults, so GC pressure is low in practice. Accepted as-is.
|
||||
internal static class SenderNameDisplay
|
||||
{
|
||||
// Returns a copy of the list with the whole ChunkSource.Sender span
|
||||
|
||||
Reference in New Issue
Block a user