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
|
// Derived from the tab list on read. Pin/Unpin/Promote/Logout simply
|
||||||
// mutate IsPinned or remove tabs — the count adapts automatically.
|
// mutate IsPinned or remove tabs — the count adapts automatically.
|
||||||
// Replaces the F2.1 Interlocked counter because the new pin-state
|
// Replaces an Interlocked counter: the pin-state transitions are cold-path
|
||||||
// transitions are cold-path and don't need lock-free reads.
|
// and don't need lock-free reads.
|
||||||
internal int ActiveTempTabCount =>
|
internal int ActiveTempTabCount =>
|
||||||
Plugin.Config.Tabs.Count(TabLifecycleHelpers.IsInUnpinnedPool);
|
Plugin.Config.Tabs.Count(TabLifecycleHelpers.IsInUnpinnedPool);
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ public class Configuration : IPluginConfiguration
|
|||||||
.PrivacyDefaults
|
.PrivacyDefaults
|
||||||
.DefaultPersistUnknownChannels;
|
.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
|
// the log every frame. NonSerialized so the warning fires once per
|
||||||
// runtime, not once-ever-per-install.
|
// runtime, not once-ever-per-install.
|
||||||
[NonSerialized]
|
[NonSerialized]
|
||||||
@@ -107,7 +107,7 @@ public class Configuration : IPluginConfiguration
|
|||||||
|
|
||||||
var known = Enum.IsDefined(typeof(ChatType), type);
|
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.
|
// recognise — i.e. one a future FFXIV patch may have added.
|
||||||
if (!known && !listed && _warnedUnknownChannels.Add(type))
|
if (!known && !listed && _warnedUnknownChannels.Add(type))
|
||||||
{
|
{
|
||||||
@@ -402,9 +402,9 @@ public class Tab
|
|||||||
|
|
||||||
public bool IsTempTab;
|
public bool IsTempTab;
|
||||||
|
|
||||||
// Pinned TempTabs survive plugin reload and logout — tester feedback from
|
// Pinned TempTabs survive plugin reload and logout -- tester feedback in
|
||||||
// Jin (v1.4.7). Pinned tabs live in their own pool (MaxPinnedTempTabs)
|
// v1.4.7. Pinned tabs live in their own pool (MaxPinnedTempTabs) separate
|
||||||
// separate from the AutoTellTabsLimit bucket.
|
// from the AutoTellTabsLimit bucket.
|
||||||
public bool IsPinned;
|
public bool IsPinned;
|
||||||
public bool AllSenderMessages;
|
public bool AllSenderMessages;
|
||||||
public TellTarget TellTarget = TellTarget.Empty();
|
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
|
// Seed the just-typed character into our input field and focus it, the
|
||||||
// same InputBar.AppendPending + Activate prefill path inventory item-links
|
// 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)
|
if (input != null)
|
||||||
{
|
{
|
||||||
Plugin.InputBar.AppendPending(input);
|
Plugin.InputBar.AppendPending(input);
|
||||||
@@ -355,9 +355,9 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
if (playerName != null)
|
if (playerName != null)
|
||||||
{
|
{
|
||||||
// Right-click -> Send Tell: prefill our input the same way our own
|
// Right-click -> Send Tell: prefill our input the same way our own
|
||||||
// "Send Tell" payload menu does (PayloadHandler), then focus. Prefill-
|
// "Send Tell" payload menu does (PayloadHandler), then focus. Prefill
|
||||||
// only — no tab switch, no ChatActivatedArgs revival (Flo decision
|
// only, deliberately: no tab switch, no ChatActivatedArgs revival.
|
||||||
// 2026-06-15). The game supplies worldName here, so no sheet lookup.
|
// The game supplies worldName here, so no sheet lookup.
|
||||||
PrefillTellInput(
|
PrefillTellInput(
|
||||||
playerName->ToString(),
|
playerName->ToString(),
|
||||||
worldName != null ? worldName->ToString() : null
|
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
|
// In-foray right-click -> Send Tell: same prefill path as the non-foray
|
||||||
// tell. The foray-specific TellSpecial channel routing stays deferred
|
// 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(
|
PrefillTellInput(
|
||||||
playerName->ToString(),
|
playerName->ToString(),
|
||||||
worldName != null ? worldName->ToString() : null
|
worldName != null ? worldName->ToString() : null
|
||||||
|
|||||||
@@ -530,7 +530,7 @@ internal unsafe class KeybindManager : IDisposable
|
|||||||
{
|
{
|
||||||
// Direct channel-switch binds (CMD_SAY/PARTY/numbered linkshells/…): switch
|
// Direct channel-switch binds (CMD_SAY/PARTY/numbered linkshells/…): switch
|
||||||
// the game channel AND mirror it onto the resolved tab so the input pill
|
// 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);
|
Plugin.Instance.Functions.Chat.SetChannel(channel);
|
||||||
// Only mirror onto the tab when the game actually accepted the switch — an
|
// 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
|
// 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)
|
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.
|
// run, so this is the first safe point to wire the setter.
|
||||||
messageList.AttachPayloadHandler(payloadHandler);
|
messageList.AttachPayloadHandler(payloadHandler);
|
||||||
|
|
||||||
@@ -172,7 +172,7 @@ internal sealed class PayloadHandlerInitHostedService(
|
|||||||
// InputBar -> CommandHelpWindow -> MainWindow -> InputBar (MS.DI does not catch
|
// InputBar -> CommandHelpWindow -> MainWindow -> InputBar (MS.DI does not catch
|
||||||
// it through FactoryCallSite registrations and the resolve recurses silently).
|
// it through FactoryCallSite registrations and the resolve recurses silently).
|
||||||
// Both singletons exist by host.StartAsync time, so this is the first safe point
|
// 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(
|
internal sealed class CommandHelpWindowInitHostedService(
|
||||||
CommandHelpWindow commandHelpWindow,
|
CommandHelpWindow commandHelpWindow,
|
||||||
MainWindow mainWindow
|
MainWindow mainWindow
|
||||||
@@ -190,7 +190,7 @@ internal sealed class CommandHelpWindowInitHostedService(
|
|||||||
// Attaches the singleton PayloadHandler to every pre-allocated pop-out
|
// Attaches the singleton PayloadHandler to every pre-allocated pop-out
|
||||||
// window's MessageList post-container-build. Pool/window cannot take the
|
// window's MessageList post-container-build. Pool/window cannot take the
|
||||||
// PayloadHandler via ctor (that would close the silent FactoryCallSite cycle —
|
// 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.
|
// AttachMainWindow). Both singletons exist by host.StartAsync time.
|
||||||
internal sealed class ChannelPopoutInitHostedService(
|
internal sealed class ChannelPopoutInitHostedService(
|
||||||
ChannelPopoutPool pool,
|
ChannelPopoutPool pool,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ internal sealed class TypingIpc : IDisposable
|
|||||||
private ICallGateProvider<ChatInputState> StateQueryGate { get; }
|
private ICallGateProvider<ChatInputState> StateQueryGate { get; }
|
||||||
private ICallGateProvider<ChatInputState, object?> StateChangedGate { 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
|
// have a no-fork policy and subscribe only to ChatTwo.*-prefixed IPC
|
||||||
// gates. HellionChat replaces ChatTwo (conflict detection prevents
|
// gates. HellionChat replaces ChatTwo (conflict detection prevents
|
||||||
// parallel loading), so mirroring the ChatTwo provider slots lets those
|
// parallel loading), so mirroring the ChatTwo provider slots lets those
|
||||||
@@ -50,7 +50,7 @@ internal sealed class TypingIpc : IDisposable
|
|||||||
"HellionChat.ChatInputStateChanged"
|
"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>(
|
ChatTwoStateQueryGate = Plugin.Interface.GetIpcProvider<ChatInputState>(
|
||||||
"ChatTwo.GetChatInputState"
|
"ChatTwo.GetChatInputState"
|
||||||
);
|
);
|
||||||
@@ -102,7 +102,7 @@ internal sealed class TypingIpc : IDisposable
|
|||||||
HasState = true;
|
HasState = true;
|
||||||
LastState = state;
|
LastState = state;
|
||||||
StateChangedGate.SendMessage(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);
|
ChatTwoStateChangedGate.SendMessage(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ internal sealed class IpcManager : IDisposable
|
|||||||
object?
|
object?
|
||||||
> InvokeGate { get; }
|
> 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
|
// a no-fork policy (e.g. Artisan, AllaganTools) only subscribe to the
|
||||||
// ChatTwo.*-prefixed context-menu integration gates. Mirroring all four
|
// ChatTwo.*-prefixed context-menu integration gates. Mirroring all four
|
||||||
// provider slots under the ChatTwo namespace lets those plugins keep
|
// provider slots under the ChatTwo namespace lets those plugins keep
|
||||||
@@ -65,7 +65,7 @@ internal sealed class IpcManager : IDisposable
|
|||||||
object?
|
object?
|
||||||
>("HellionChat.Invoke");
|
>("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
|
// above. Share the same Register/Unregister backing methods so a
|
||||||
// plugin that subscribes via either namespace lands in the same
|
// plugin that subscribes via either namespace lands in the same
|
||||||
// Registered list. SendMessage on Invoke fans out to both gates.
|
// 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);
|
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);
|
ChatTwoInvokeGate.SendMessage(id, sender, contentId, payload, senderString, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -248,9 +248,8 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
_logger.LogError(ex, "Error in FilterAllTabs");
|
_logger.LogError(ex, "Error in FilterAllTabs");
|
||||||
}
|
}
|
||||||
|
|
||||||
// v1.4.9 R3 profiling: Information so the xllog tail surfaces this
|
// Information, not Debug, so the xllog tail surfaces this without a
|
||||||
// without a Debug filter. Belt-and-suspenders for future plugin-load
|
// filter. Kept as a guard against future plugin-load regressions.
|
||||||
// regressions; remains in place after Sub-Task 3.4 Befund.
|
|
||||||
_logger.LogInformation($"FilterAllTabs took {stopwatch.ElapsedMilliseconds}ms");
|
_logger.LogInformation($"FilterAllTabs took {stopwatch.ElapsedMilliseconds}ms");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -436,10 +435,10 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
// TEST-MIRROR: ../_Helpers/TabSoundDecision.cs
|
// TEST-MIRROR: ../_Helpers/TabSoundDecision.cs
|
||||||
// Unseen ("count only what you haven't seen") suppresses unread on an inactive
|
// 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 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 you're looking at (1.5.6 / upstream ChatTwo behavior). The "active tab"
|
||||||
// tab" was wrongly pinned to Tabs[0], so this fired against the wrong tab; F2
|
// used to be pinned to Tabs[0], so this fired against the wrong one until
|
||||||
// recoupled CurrentTab to the REAL active tab, so currentTabMatches is now
|
// CurrentTab was recoupled to the real active tab, and currentTabMatches is
|
||||||
// measured against the tab you actually see. All -> always counts; None ->
|
// now measured against the tab you see. All -> always counts; None ->
|
||||||
// counts here and is gated out at the display layer. Pure + SelfTest-able.
|
// counts here and is gated out at the display layer. Pure + SelfTest-able.
|
||||||
internal static bool ShouldCountUnread(Tab tab, Tab currentTab, bool currentTabMatches) =>
|
internal static bool ShouldCountUnread(Tab tab, Tab currentTab, bool currentTabMatches) =>
|
||||||
!(
|
!(
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ internal class MessageStore : IDisposable
|
|||||||
|
|
||||||
private SqliteConnection Connect()
|
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
|
// with the Migrate-Stopwatch below — Connect alone is the cheap half
|
||||||
// (Open + a handful of PRAGMAs); the expensive half typically lives in
|
// (Open + a handful of PRAGMAs); the expensive half typically lives in
|
||||||
// Migrate, especially on a large DB after a schema bump.
|
// Migrate, especially on a large DB after a schema bump.
|
||||||
@@ -260,7 +260,7 @@ internal class MessageStore : IDisposable
|
|||||||
|
|
||||||
private void Migrate()
|
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
|
// large DB after a fresh schema bump this is the dominant SQLite cost
|
||||||
// at plugin-load, not Connect.
|
// at plugin-load, not Connect.
|
||||||
var migrateSw = System.Diagnostics.Stopwatch.StartNew();
|
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
|
// 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 --
|
// 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
|
// 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
|
// guaranteed; callers re-sort (DbViewer sorts by Date descending).
|
||||||
// Sub-Task 4.4).
|
|
||||||
public IReadOnlyList<Message> LoadByGuids(IReadOnlyList<string> guidStrings)
|
public IReadOnlyList<Message> LoadByGuids(IReadOnlyList<string> guidStrings)
|
||||||
{
|
{
|
||||||
if (guidStrings.Count == 0)
|
if (guidStrings.Count == 0)
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ internal sealed class PayloadHandler
|
|||||||
// Eureka, Bozja and Occult need special handling as tells work different
|
// Eureka, Bozja and Occult need special handling as tells work different
|
||||||
if (!Sheets.IsInForay())
|
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
|
// XC-8: shares the /tell builder with the native detours. IsPublic (not
|
||||||
// IsNullOrEmpty) is resolved HERE — a private/null world must NOT leak @World.
|
// IsNullOrEmpty) is resolved HERE — a private/null world must NOT leak @World.
|
||||||
_inputBar.SetPendingMessage(
|
_inputBar.SetPendingMessage(
|
||||||
@@ -393,7 +393,7 @@ internal sealed class PayloadHandler
|
|||||||
var inputChannel = chunk.Message?.Code.Type.ToInputChannel();
|
var inputChannel = chunk.Message?.Code.Type.ToInputChannel();
|
||||||
if (inputChannel != null && ImGui.Selectable(Language.Context_ReplyInSelectedChatMode))
|
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);
|
_mainWindow.ActiveTab?.CurrentChannel?.SetChannel(inputChannel.Value);
|
||||||
_inputBar.Activate = true;
|
_inputBar.Activate = true;
|
||||||
}
|
}
|
||||||
@@ -731,7 +731,7 @@ internal sealed class PayloadHandler
|
|||||||
using (ImRaii.Tooltip())
|
using (ImRaii.Tooltip())
|
||||||
using (ImRaii.TextWrapPos(0.0f))
|
using (ImRaii.TextWrapPos(0.0f))
|
||||||
using (
|
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(
|
ImRaii.PushColor(
|
||||||
ImGuiCol.Text,
|
ImGuiCol.Text,
|
||||||
ColourUtil.RgbaToVector4(_themes.Active.Colors.TextPrimary)
|
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
|
// 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
|
// 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.
|
// deliberate divergence from Lightless' deferred Func-delegate pattern.
|
||||||
internal static class PluginHostFactory
|
internal static class PluginHostFactory
|
||||||
{
|
{
|
||||||
@@ -48,7 +48,7 @@ internal static class PluginHostFactory
|
|||||||
PluginHostDependencies dependencies
|
PluginHostDependencies dependencies
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
// Block A — Dalamud services (21 [PluginService] singletons).
|
// Dalamud services (21 [PluginService] singletons).
|
||||||
services.AddSingleton(dependencies);
|
services.AddSingleton(dependencies);
|
||||||
services.AddSingleton(dependencies.PluginInterface);
|
services.AddSingleton(dependencies.PluginInterface);
|
||||||
services.AddSingleton(dependencies.PluginLog);
|
services.AddSingleton(dependencies.PluginLog);
|
||||||
@@ -77,7 +77,7 @@ internal static class PluginHostFactory
|
|||||||
services.AddSingleton(plugin.WindowSystem);
|
services.AddSingleton(plugin.WindowSystem);
|
||||||
services.AddSingleton<PluginLifecycle>();
|
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
|
// classes are internal-sealed and the default activator only sees
|
||||||
// public ctors.
|
// public ctors.
|
||||||
services.AddSingleton<IPlatformUtil>(_ => new DalamudPlatformUtil());
|
services.AddSingleton<IPlatformUtil>(_ => new DalamudPlatformUtil());
|
||||||
@@ -307,7 +307,7 @@ internal static class PluginHostFactory
|
|||||||
// Pop-out windows: each gets its OWN MessageList + InputBar so the
|
// Pop-out windows: each gets its OWN MessageList + InputBar so the
|
||||||
// channel pill and message scroll are per-window. The PayloadHandler is
|
// channel pill and message scroll are per-window. The PayloadHandler is
|
||||||
// attached post-build (ChannelPopoutInitHostedService), NEVER via ctor
|
// 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 =>
|
services.AddSingleton<Func<int, Ui.Windows.ChannelPopoutWindow>>(sp =>
|
||||||
slot => new Ui.Windows.ChannelPopoutWindow(
|
slot => new Ui.Windows.ChannelPopoutWindow(
|
||||||
slot,
|
slot,
|
||||||
@@ -336,7 +336,7 @@ internal static class PluginHostFactory
|
|||||||
sp.GetRequiredService<ILogger<Ui.Windows.ChannelPopoutPool>>()
|
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.
|
// PluginLifecycle.LoadAsync on the framework thread.
|
||||||
services.AddSingleton(sp => new Ui.Windows.SettingsWindow(
|
services.AddSingleton(sp => new Ui.Windows.SettingsWindow(
|
||||||
sp.GetRequiredService<Plugin>(),
|
sp.GetRequiredService<Plugin>(),
|
||||||
@@ -380,8 +380,8 @@ internal static class PluginHostFactory
|
|||||||
));
|
));
|
||||||
#endif
|
#endif
|
||||||
// The style lab: variants side by side, in-game, against the live theme.
|
// The style lab: variants side by side, in-game, against the live theme.
|
||||||
// Permanent by Flo's call, and deliberately not behind DEBUG -- style
|
// Permanent, and deliberately not behind DEBUG: style decisions get
|
||||||
// decisions happen in the build he actually runs.
|
// made in the build that actually ships.
|
||||||
services.AddSingleton(sp => new Ui.Windows.InputBarLabWindow(
|
services.AddSingleton(sp => new Ui.Windows.InputBarLabWindow(
|
||||||
sp.GetRequiredService<Plugin>()
|
sp.GetRequiredService<Plugin>()
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ namespace HellionChat.Privacy;
|
|||||||
|
|
||||||
internal static class PrivacyDefaults
|
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
|
// 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
|
// dropped before the user can opt in or out. Existing configs keep their
|
||||||
// explicit choice — see Configuration.cs PrivacyPersistUnknownChannels.
|
// explicit choice — see Configuration.cs PrivacyPersistUnknownChannels.
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ internal sealed class CardClipPlanStep : ISelfTestStep
|
|||||||
int remaining;
|
int remaining;
|
||||||
try
|
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.
|
// the step walks a synthetic clock past the window instead of sleeping.
|
||||||
var clock = Environment.TickCount64;
|
var clock = Environment.TickCount64;
|
||||||
messages.RunHeightCacheInvalidationForSelfTest(tab, 400f, clock);
|
messages.RunHeightCacheInvalidationForSelfTest(tab, 400f, clock);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using Dalamud.Plugin.SelfTest;
|
|||||||
|
|
||||||
namespace HellionChat.SelfTests;
|
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
|
// Tabs lookup). Asserts ReferenceEquals between the two, with false-green
|
||||||
// defenses: (1) empty-config exercises the getter's fallback; (2) null ActiveTab
|
// 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
|
// 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;
|
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
|
// 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
|
// through the real switch-away-and-back flow. It verifies the PRIVACY-relevant
|
||||||
// effect, keyed on the tab type:
|
// effect, keyed on the tab type:
|
||||||
// - a NORMAL tab carrying a game-side tell must lose its RUNTIME target
|
// - 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;
|
// typed line can't /tell the old partner;
|
||||||
// - a BOUND auto-tell tab keeps its partner by design (leg1) — its binding is
|
// - a BOUND auto-tell tab keeps its partner by design (leg1) — its binding is
|
||||||
// Tab.TellTarget and is deliberately untouched by the strip.
|
// Tab.TellTarget and is deliberately untouched by the strip.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ using HellionChat.Util;
|
|||||||
|
|
||||||
namespace HellionChat.SelfTests;
|
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
|
// raw SeStrings. That change is invisible to the build suite -- ExportToFile
|
||||||
// takes IEnumerable<Message>, Message needs SeString, and xUnit cannot load
|
// takes IEnumerable<Message>, Message needs SeString, and xUnit cannot load
|
||||||
// Dalamud.dll, so even an empty list fails before the body runs.
|
// Dalamud.dll, so even an empty list fails before the body runs.
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ internal sealed class FontManagerCtorSmokeStep : ISelfTestStep
|
|||||||
return SelfTestStepResult.Fail;
|
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
|
// 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.
|
// range is now a small trimmed remainder next to the large primary range.
|
||||||
var counts = fm.GlyphRangeLengths;
|
var counts = fm.GlyphRangeLengths;
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ internal sealed class GlobalStyleScopeAllocStep : ISelfTestStep
|
|||||||
GlobalStyleScope.Push(theme, registry, opacity).Dispose();
|
GlobalStyleScope.Push(theme, registry, opacity).Dispose();
|
||||||
var delta = GC.GetAllocatedBytesForCurrentThread() - before;
|
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.
|
// show Pass) — the byte delta is the whole point of the GC-reserve probe.
|
||||||
var ok = delta <= AllocBudgetBytes;
|
var ok = delta <= AllocBudgetBytes;
|
||||||
var status = ok ? "PASS" : "FAIL";
|
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
|
// 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
|
try
|
||||||
{
|
{
|
||||||
// (a) available + valid title + toggle on -> title renders
|
// (a) available + valid title + toggle on -> title renders
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ using HellionChat.Ui.StyleEngine;
|
|||||||
|
|
||||||
namespace HellionChat.SelfTests;
|
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
|
// Successor to HoverSheenAllocStep, which pinned the same contract against the
|
||||||
// old sheen start-timestamp dictionary.
|
// 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
|
// B1-2 window flags. Drives the REAL MainWindow.PreDraw and asserts it wired
|
||||||
// Window.Flags to ResolveFlags(CanMove, CanResize), then pins the pure
|
// Window.Flags to ResolveFlags(CanMove, CanResize), then pins the pure
|
||||||
// fresh-base contract: false/false adds NoMove|NoResize, true/true clears them
|
// 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
|
// -- flags must rebuild from a fresh base, or NoMove sticks after toggling
|
||||||
// after toggling back" risk). NoScrollbar|NoScrollWithMouse always present.
|
// back. NoScrollbar|NoScrollWithMouse always present.
|
||||||
// Non-test caller of ResolveFlags: MainWindow.PreDraw.
|
// Non-test caller of ResolveFlags: MainWindow.PreDraw.
|
||||||
internal sealed class MainWindowFlagsStep : ISelfTestStep
|
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
|
// step so the per-frame hot path never references file IO. Writes one
|
||||||
// perf-baseline.json into the plugin ConfigDirectory, atomically (tmp + move)
|
// perf-baseline.json into the plugin ConfigDirectory, atomically (tmp + move)
|
||||||
// like ThemeRegistry's theme writer, so a mid-write crash leaves either the
|
// 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
|
// 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
|
// (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.
|
// 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
|
// Optional metric capture. Accumulates 1000 steady-state frames of ImGui IO
|
||||||
// counters plus the plugin's full-Draw wall-time (Plugin.LastDrawMs, B5-1),
|
// 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
|
// 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
|
// 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
|
// manual"). It returns Waiting until the sample window fills, mirroring the
|
||||||
// per-frame poll idiom of ThemeSwitchSelfTestStep.
|
// per-frame poll idiom of ThemeSwitchSelfTestStep.
|
||||||
internal sealed class PerformanceBaselineStep : ISelfTestStep
|
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.
|
// average out GC blips without making the manual step tedious.
|
||||||
private const int TargetFrames = 1000;
|
private const int TargetFrames = 1000;
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ using HellionChat.Code;
|
|||||||
|
|
||||||
namespace HellionChat.SelfTests;
|
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
|
// Sidebar.Draw (render precedent: SidebarGreetedGlyphStep) with a probe tab that
|
||||||
// is inactive and carries Unread>0, then reads the render-observability counter
|
// 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;
|
// so a regressed/absent dot fails. Asserts: dot drawn for an inactive Unseen tab;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ using HellionChat.Util;
|
|||||||
|
|
||||||
namespace HellionChat.SelfTests;
|
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
|
// 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
|
// — 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,
|
// five contracts: strip-on-switch, no-strip-on-reclick (TR-4), leg1 preserve,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ using HellionChat.Ui.StyleEngine;
|
|||||||
|
|
||||||
namespace HellionChat.SelfTests;
|
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
|
// so without this step block A would end with nothing to look at and two helpers
|
||||||
// (TypeScale, BaselineMath) with no caller at all.
|
// (TypeScale, BaselineMath) with no caller at all.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ using Dalamud.Plugin.SelfTest;
|
|||||||
|
|
||||||
namespace HellionChat.SelfTests;
|
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
|
// 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
|
// saw it there) -- 1.5.6/upstream semantics, measured against the real active
|
||||||
// tab thanks to F2. Asserts the truth table: suppressed when active tab also
|
// tab. Asserts the truth table: suppressed when the active tab also matches;
|
||||||
// matches; counts when it does not (the Carla/Jin case); All always counts; None
|
// counts when it does not; All always counts; None counts at the increment
|
||||||
// counts at the increment layer (the display gate hides it).
|
// layer (the display gate hides it).
|
||||||
internal sealed class UnreadDecisionStep : ISelfTestStep
|
internal sealed class UnreadDecisionStep : ISelfTestStep
|
||||||
{
|
{
|
||||||
public string Name => "Hellion Chat - Unread decision (per active tab)";
|
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
|
// (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))
|
if (!MessageManager.ShouldCountUnread(inactive, active, currentTabMatches: false))
|
||||||
{
|
{
|
||||||
ImGui.Text("(b) inactive Unseen tab must count when the active tab does not show it");
|
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
|
// jumps straight to Step 4 (no Step-3 entry → no seed for
|
||||||
// FilterIncludePreviousSessions), commits, and asserts the history
|
// FilterIncludePreviousSessions), commits, and asserts the history
|
||||||
// toggle remained on its pre-test value. Pins the null-semantics
|
// 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
|
// writing seeded recommendations unconditionally would surface
|
||||||
// here.
|
// here.
|
||||||
// CommitPending → ApplyRoleplay overwrites six privacy /
|
// CommitPending → ApplyRoleplay overwrites six privacy /
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using Microsoft.Extensions.Logging;
|
|||||||
namespace HellionChat.Services;
|
namespace HellionChat.Services;
|
||||||
|
|
||||||
// Routes an incoming tell to the configured TellAutoOpenMode (Off/Sidebar/
|
// 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
|
// 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
|
// finds. Popout guards on pool.IsOpen so it never double-pops a tab the
|
||||||
// AutoTellTabsOpenAsPopout path already opened. Subscribes to the resolved
|
// AutoTellTabsOpenAsPopout path already opened. Subscribes to the resolved
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ public sealed class ThemeRegistry
|
|||||||
internal void SetActiveChangedCallback(Action callback) => _onActiveChanged = callback;
|
internal void SetActiveChangedCallback(Action callback) => _onActiveChanged = callback;
|
||||||
|
|
||||||
// Shared slug guard for any code path that turns a slug into a filename.
|
// 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.
|
// path-traversal/invalid-char rules live in exactly one place.
|
||||||
//
|
//
|
||||||
// Whitespace rejection is intentional: Path.GetInvalidFileNameChars on
|
// Whitespace rejection is intentional: Path.GetInvalidFileNameChars on
|
||||||
@@ -112,7 +112,7 @@ public sealed class ThemeRegistry
|
|||||||
public Theme Active => _active;
|
public Theme Active => _active;
|
||||||
|
|
||||||
// Read-only exposure of the configured custom themes directory.
|
// 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;
|
public string? CustomThemesDir => _customThemesDir;
|
||||||
|
|
||||||
// Read-only enumeration of all built-in theme slugs. T2 ThemePickerCategoryStep
|
// 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;
|
public IEnumerable<string> BuiltinSlugs => _builtIns.Keys;
|
||||||
|
|
||||||
// True try-pattern lookup: returns false when neither built-in nor custom
|
// True try-pattern lookup: returns false when neither built-in nor custom
|
||||||
// cache holds the slug, no fallback to default. M3 ThemePicker uses this
|
// cache holds the slug, no fallback to default. ThemePicker uses this
|
||||||
// for card-rendering, M6 ThemeImportExportRow for fork-slug collisions.
|
// for card-rendering, ThemeImportExportRow for fork-slug collisions.
|
||||||
// Cold-cache fallback: see `LoadCustomBySlug` lookup-by-slug reverse
|
// Cold-cache fallback: see `LoadCustomBySlug` lookup-by-slug reverse
|
||||||
// iteration — it only walks the pre-populated _customCache. If a freshly
|
// 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
|
// 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
|
// Switch() prefers built-ins over custom themes with the same slug
|
||||||
// (see `Switch` built-in-first lookup), so saving a custom file under
|
// (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 —
|
// 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
|
// built-in themes into a custom slug before BeginEditing, M6
|
||||||
// ImportFromPath renames built-in-colliding imports to <slug>_imported.
|
// ImportFromPath renames built-in-colliding imports to <slug>_imported.
|
||||||
// New call-sites must either fork first or rename to a non-built-in slug.
|
// 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.
|
// separators, parent-directory tokens, or platform-invalid filename chars.
|
||||||
// Without this guard an imported theme with Slug "../../../etc/passwd"
|
// Without this guard an imported theme with Slug "../../../etc/passwd"
|
||||||
// would let Path.Combine escape _customThemesDir entirely. Shared helper
|
// 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;
|
var safeSlug = _editingThemeBuffer.Slug;
|
||||||
if (!IsSafeThemeSlug(safeSlug))
|
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,
|
// 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
|
// 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
|
// below returns false. The caller then sees "save failed" while a garbage
|
||||||
// file accumulates in the themes dir on every retry. M4 ColorPicker forks
|
// file accumulates in the themes dir on every retry. ColorPicker forks
|
||||||
// built-in themes into a custom slug before BeginEditing, M6 ImportFromPath
|
// built-in themes into a custom slug before BeginEditing, ImportFromPath
|
||||||
// renames built-in-colliding imports to <slug>_imported, so production
|
// renames built-in-colliding imports to <slug>_imported, so production
|
||||||
// paths already steer clear; this guard catches everything else.
|
// paths already steer clear; this guard catches everything else.
|
||||||
if (_builtIns.ContainsKey(safeSlug))
|
if (_builtIns.ContainsKey(safeSlug))
|
||||||
@@ -522,8 +522,8 @@ public sealed class ThemeRegistry
|
|||||||
)
|
)
|
||||||
{
|
{
|
||||||
var t = (float)(now - _crossfadeStartTickMs) / CrossfadeDurationMs;
|
var t = (float)(now - _crossfadeStartTickMs) / CrossfadeDurationMs;
|
||||||
// A2: SmoothStep easing so the fade eases in/out instead of a
|
// SmoothStep easing so the fade eases in and out instead of running
|
||||||
// linear ramp. MUST stay in lockstep with TryGetActiveCrossfade (K8).
|
// linear. MUST stay in lockstep with TryGetActiveCrossfade.
|
||||||
var te = t * t * (3f - 2f * t);
|
var te = t * t * (3f - 2f * t);
|
||||||
snapshot = ThemeAbgrCacheLerp.Lerp(_previousAbgrSnapshot.Value, _active.AbgrCache, te);
|
snapshot = ThemeAbgrCacheLerp.Lerp(_previousAbgrSnapshot.Value, _active.AbgrCache, te);
|
||||||
}
|
}
|
||||||
@@ -551,7 +551,7 @@ public sealed class ThemeRegistry
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
var t = (float)elapsed / CrossfadeDurationMs;
|
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);
|
var te = t * t * (3f - 2f * t);
|
||||||
lerped = ThemeAbgrCacheLerp.Lerp(_previousAbgrSnapshot.Value, _active.AbgrCache, te);
|
lerped = ThemeAbgrCacheLerp.Lerp(_previousAbgrSnapshot.Value, _active.AbgrCache, te);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ internal sealed class CommandHelpWindow : Window
|
|||||||
// Setter-injected post-ctor to break the InputBar -> CommandHelpWindow ->
|
// Setter-injected post-ctor to break the InputBar -> CommandHelpWindow ->
|
||||||
// MainWindow -> InputBar singleton cycle (MS.DI does not detect cycles
|
// MainWindow -> InputBar singleton cycle (MS.DI does not detect cycles
|
||||||
// through FactoryCallSite registrations). Wired in
|
// through FactoryCallSite registrations). Wired in
|
||||||
// CommandHelpWindowInitHostedService.StartAsync, same §6.2 pattern as
|
// CommandHelpWindowInitHostedService.StartAsync, same setter-injection pattern as
|
||||||
// MessageList.AttachPayloadHandler.
|
// MessageList.AttachPayloadHandler.
|
||||||
private Windows.MainWindow? _mainWindow;
|
private Windows.MainWindow? _mainWindow;
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ internal sealed class CommandHelpWindow : Window
|
|||||||
RespectCloseHotkey = false;
|
RespectCloseHotkey = false;
|
||||||
DisableWindowSounds = true;
|
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;
|
_ = _logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using System.Collections.Generic;
|
|||||||
namespace HellionChat.Ui.Components;
|
namespace HellionChat.Ui.Components;
|
||||||
|
|
||||||
// B2 (PERF-B2): variable-height clip plan. ImGuiListClipper needs a constant
|
// 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
|
// too), so both compute a plan from the cached per-row heights: a lead dummy
|
||||||
// for the rows above
|
// for the rows above
|
||||||
// the viewport, the [first..last] index range that overlaps the viewport, and
|
// 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
|
// Note: when MainWindow is closed, DrawInputField never runs, so
|
||||||
// _isFocused keeps the last value written by the previous draw pass.
|
// _isFocused keeps the last value written by the previous draw pass.
|
||||||
// The consumer that actually pushes this state across the IPC boundary
|
// 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
|
// itself, so the stale backing-field never leaks to subscribers. Mirroring
|
||||||
// the gate here would require an extra Plugin-backref in InputBar that the
|
// the gate here would require an extra Plugin-backref in InputBar that the
|
||||||
// rest of the component doesn't need.
|
// 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
|
// 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
|
internal sealed class AutoCompleteInfo
|
||||||
{
|
{
|
||||||
// ToComplete MUST be a mutable field (not an auto-property), because the
|
// 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;
|
private readonly Action<Message, string?> _drawCardRow;
|
||||||
|
|
||||||
// Reused across frames: at MessageManager.MessageDisplayLimit a fresh array
|
// 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
|
// path. The old comment named MaxLinesToRender and its 2500 default, a
|
||||||
// config field that had stopped bounding anything.
|
// config field that had stopped bounding anything.
|
||||||
private float[] _heightScratch = [];
|
private float[] _heightScratch = [];
|
||||||
@@ -49,7 +49,7 @@ internal sealed class MessageList
|
|||||||
private bool _stampVisible;
|
private bool _stampVisible;
|
||||||
private float _metaDrop;
|
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.
|
// Wired by PayloadHandlerInitHostedService.StartAsync after both singletons exist.
|
||||||
internal void AttachPayloadHandler(PayloadHandler handler)
|
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
|
// 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
|
// 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)
|
internal int RunHeightCacheInvalidationForSelfTest(Tab tab, float contentWidth, long nowMs)
|
||||||
{
|
{
|
||||||
InvalidateHeightCacheIfLayoutChanged(tab, contentWidth, 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
|
// 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
|
// 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
|
// 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.
|
// text here.
|
||||||
// A system message has no sender, so a header row would be a stamp on a
|
// 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
|
// 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) =>
|
private static uint Abgr(StyleEngine.Token token, ThemeColors colors) =>
|
||||||
ColourUtil.RgbaToAbgr(Tokens.Resolve(token, 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
|
// counter must read 0 (plugin disabled) or 1 (plugin enabled). Anything
|
||||||
// higher signals a Dispose skip and a subscriber leak against ThemeRegistry.
|
// higher signals a Dispose skip and a subscriber leak against ThemeRegistry.
|
||||||
internal static int InstanceCount;
|
internal static int InstanceCount;
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ internal sealed class DataPrivacyTab
|
|||||||
)
|
)
|
||||||
{
|
{
|
||||||
// Read-only statement, not a switch. Do not promote it to one
|
// 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.
|
// something to turn off.
|
||||||
ImGuiUtil.HelpText(HellionStrings.Settings_Telemetry_None);
|
ImGuiUtil.HelpText(HellionStrings.Settings_Telemetry_None);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,8 +173,7 @@ internal sealed class ThemeImportExportRow
|
|||||||
// Slug sanitisation BEFORE BeginEditing — SaveEditingBuffer would
|
// Slug sanitisation BEFORE BeginEditing — SaveEditingBuffer would
|
||||||
// reject too, but rejecting here means an unsafe slug never enters
|
// reject too, but rejecting here means an unsafe slug never enters
|
||||||
// the editing buffer. Shared helper ThemeRegistry.IsSafeThemeSlug
|
// the editing buffer. Shared helper ThemeRegistry.IsSafeThemeSlug
|
||||||
// keeps the rule set in sync with F1's save-side guard (see
|
// keeps the rule set in sync with the save-side guard.
|
||||||
// ThemeRegistry.IsSafeThemeSlug shared helper).
|
|
||||||
var importSlug = theme.Slug;
|
var importSlug = theme.Slug;
|
||||||
if (!ThemeRegistry.IsSafeThemeSlug(importSlug))
|
if (!ThemeRegistry.IsSafeThemeSlug(importSlug))
|
||||||
{
|
{
|
||||||
@@ -186,9 +185,9 @@ internal sealed class ThemeImportExportRow
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pragmatic deviation from §1.6 wording ("File.Copy into themes/"):
|
// Not a plain File.Copy into themes/: BeginEditing+SaveEditingBuffer
|
||||||
// BeginEditing+SaveEditingBuffer produces the same end-state and
|
// produces the same end state and reuses the validated save
|
||||||
// reuses the validated F1 save pipeline. Trade-off: destination
|
// pipeline. Trade-off: destination
|
||||||
// filename becomes the theme's slug, not the original filename.
|
// filename becomes the theme's slug, not the original filename.
|
||||||
//
|
//
|
||||||
// Slug-collision handling:
|
// Slug-collision handling:
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ internal sealed class InputPreview : Window
|
|||||||
DisableWindowSounds = true;
|
DisableWindowSounds = true;
|
||||||
IsOpen = true;
|
IsOpen = true;
|
||||||
|
|
||||||
// TODO Polish-Sweep: remove discard once logging call-sites exist
|
// TODO: remove discard once logging call sites exist
|
||||||
_ = _logger;
|
_ = _logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ namespace HellionChat.Ui.StyleEngine;
|
|||||||
// ABGR before delegating to ImDrawList.
|
// ABGR before delegating to ImDrawList.
|
||||||
internal static class DrawListExtensions
|
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
|
// the element's accent hue. Kept low so the sheen reads as a tinted
|
||||||
// highlight, not a saturated accent flash (effect level "subtle").
|
// highlight, not a saturated accent flash (effect level "subtle").
|
||||||
private const float SheenTintStrength = 0.35f;
|
private const float SheenTintStrength = 0.35f;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ namespace HellionChat.Ui.StyleEngine;
|
|||||||
// compare an unscaled value (Sidebar.GetWidth, the width slider bounds).
|
// compare an unscaled value (Sidebar.GetWidth, the width slider bounds).
|
||||||
//
|
//
|
||||||
// Deliberately NOT part of ThemeLayout: that record is serialised into theme
|
// 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
|
internal static class Metrics
|
||||||
{
|
{
|
||||||
// --- Sidebar ---
|
// --- Sidebar ---
|
||||||
@@ -65,7 +65,7 @@ internal static class Metrics
|
|||||||
return _cachedScale;
|
return _cachedScale;
|
||||||
|
|
||||||
// Safe variant: GlobalScale throws while the interface manager is
|
// 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;
|
_cachedScale = ImGuiHelpers.GlobalScaleSafe;
|
||||||
_cachedFrame = frame;
|
_cachedFrame = frame;
|
||||||
return _cachedScale;
|
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
|
// exactly Body and never be pushed -- a value with no call site, which is the one
|
||||||
// thing this whole style track exists to stop.
|
// 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
|
// 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
|
// 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.
|
// 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
|
// 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
|
// 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
|
// wrong. Picked from the lab over the rule-only variant; at this strength it
|
||||||
// it survives the violet themes that killed the filled bar.
|
// 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
|
// 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
|
// 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
|
// windows via the injected factory, all registered once in the WindowSystem
|
||||||
// (PluginLifecycle.RegisterWindows, framework thread). Open/Close is IsOpen +
|
// (PluginLifecycle.RegisterWindows, framework thread). Open/Close is IsOpen +
|
||||||
// Bind/Unbind only — NEVER runtime AddWindow/RemoveWindow (v1.4.9 Stage-2
|
// 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
|
internal sealed class ChannelPopoutPool
|
||||||
{
|
{
|
||||||
private readonly List<ChannelPopoutWindow> _instances;
|
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
|
// 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
|
// 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)
|
foreach (var window in _instances)
|
||||||
window.CloseRequested = TryClose;
|
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
|
// One pre-allocated pop-out window bound to a single Tab. Pure DI-sink: the
|
||||||
// PayloadHandler arrives via AttachPayloadHandler (post-build setter), NEVER
|
// 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
|
// instances are unique for WindowSystem.AddWindow and ImGui state is stable
|
||||||
// per slot (not per bound tab).
|
// per slot (not per bound tab).
|
||||||
internal sealed class ChannelPopoutWindow : Window, IFocusableChatWindow
|
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
|
// 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
|
// (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; }
|
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) =>
|
public void AttachPayloadHandler(PayloadHandler handler) =>
|
||||||
_messages.AttachPayloadHandler(handler);
|
_messages.AttachPayloadHandler(handler);
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ internal sealed class ChannelPopoutWindow : Window, IFocusableChatWindow
|
|||||||
Bound = tab;
|
Bound = tab;
|
||||||
|
|
||||||
var isTell = tab is { IsTempTab: true, TellTarget: { } target } && target.IsSet();
|
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);
|
Size = isTell ? new Vector2(380f, 320f) : new Vector2(420f, 320f);
|
||||||
SizeCondition = ImGuiCond.FirstUseEver;
|
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
|
// Variants of UI elements, drawn side by side so decisions are made by looking
|
||||||
// rather than by imagining. Reachable with /hellion lab.
|
// rather than by imagining. Reachable with /hellion lab.
|
||||||
//
|
//
|
||||||
// Permanent, by Flo's call: a dev playground for seeing ideas in-game against
|
// Permanent: a playground for seeing ideas in-game against the live theme.
|
||||||
// the live theme. The radios default to whatever shipped, so the window also
|
// The radios default to whatever shipped, so the window also
|
||||||
// documents which variant won and what it beat.
|
// documents which variant won and what it beat.
|
||||||
//
|
//
|
||||||
// It exists because the alternative was drawing mockups, and mockups are what
|
// It exists because the alternative was drawing mockups, and mockups are what
|
||||||
|
|||||||
@@ -62,9 +62,9 @@ internal static class AutoTranslate
|
|||||||
{
|
{
|
||||||
var sw = Stopwatch.StartNew();
|
var sw = Stopwatch.StartNew();
|
||||||
AllEntries();
|
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
|
// 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(
|
Plugin.LogProxy.Information(
|
||||||
$"Warming up auto-translate took {sw.ElapsedMilliseconds}ms"
|
$"Warming up auto-translate took {sw.ElapsedMilliseconds}ms"
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,244 +9,6 @@ namespace HellionChat.Util;
|
|||||||
|
|
||||||
internal static class ChunkUtil
|
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(
|
internal static IEnumerable<Chunk> ToChunks(
|
||||||
SeString msg,
|
SeString msg,
|
||||||
ChunkSource source,
|
ChunkSource source,
|
||||||
@@ -499,47 +261,4 @@ internal static class ChunkUtil
|
|||||||
|
|
||||||
return BitConverter.ToUInt32(numArray, 0);
|
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.
|
// a later settings change reformats history too.
|
||||||
// Known trade-off: at non-default settings, this allocates one List<Chunk> per
|
// 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
|
// 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
|
// neutral defaults, so GC pressure is low in practice. Accepted as-is.
|
||||||
// Cycle Notes.
|
|
||||||
internal static class SenderNameDisplay
|
internal static class SenderNameDisplay
|
||||||
{
|
{
|
||||||
// Returns a copy of the list with the whole ChunkSource.Sender span
|
// Returns a copy of the list with the whole ChunkSource.Sender span
|
||||||
|
|||||||
Reference in New Issue
Block a user