Files
HellionChat/HellionChat/PluginHostFactory.cs
T
JonKazama-Hellion 5b738e6885 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.
2026-08-19 21:50:31 +02:00

481 lines
23 KiB
C#

using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using HellionChat.Infrastructure.Hosting;
using HellionChat.Infrastructure.Logging;
using HellionChat.Ipc;
using HellionChat.Themes;
using HellionChat.Ui;
using HellionChat.Util;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
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. For the
// deliberate divergence from Lightless' deferred Func-delegate pattern.
internal static class PluginHostFactory
{
public static IHost Build(Plugin plugin, PluginHostDependencies dependencies)
{
return new HostBuilder()
.UseContentRoot(dependencies.PluginInterface.ConfigDirectory.FullName)
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.AddDalamudLogging(dependencies.PluginLog);
logging.SetMinimumLevel(LogLevel.Trace);
})
// ValidateOnBuild eagerly instantiates every singleton at Build time
// so missing registrations / ConstructorCallSite cycles throw on
// load instead of producing a silent hang. ValidateScopes is cheap
// (we only use singletons) but guards against future Scoped misuse.
.UseDefaultServiceProvider(o =>
{
o.ValidateOnBuild = true;
o.ValidateScopes = true;
})
.ConfigureServices(services => ConfigureServices(services, plugin, dependencies))
.Build();
}
private static void ConfigureServices(
IServiceCollection services,
Plugin plugin,
PluginHostDependencies dependencies
)
{
// Dalamud services (21 [PluginService] singletons).
services.AddSingleton(dependencies);
services.AddSingleton(dependencies.PluginInterface);
services.AddSingleton(dependencies.PluginLog);
services.AddSingleton(dependencies.ChatGui);
services.AddSingleton(dependencies.ClientState);
services.AddSingleton(dependencies.CommandManager);
services.AddSingleton(dependencies.Condition);
services.AddSingleton(dependencies.DataManager);
services.AddSingleton(dependencies.Framework);
services.AddSingleton(dependencies.GameGui);
services.AddSingleton(dependencies.KeyState);
services.AddSingleton(dependencies.ObjectTable);
services.AddSingleton(dependencies.PartyList);
services.AddSingleton(dependencies.TargetManager);
services.AddSingleton(dependencies.TextureProvider);
services.AddSingleton(dependencies.GameInteropProvider);
services.AddSingleton(dependencies.GameConfig);
services.AddSingleton(dependencies.Notification);
services.AddSingleton(dependencies.AddonLifecycle);
services.AddSingleton(dependencies.PlayerState);
services.AddSingleton(dependencies.Evaluator);
services.AddSingleton(dependencies.SelfTestRegistry);
// Self-references: Plugin and its WindowSystem already exist.
services.AddSingleton(plugin);
services.AddSingleton(plugin.WindowSystem);
services.AddSingleton<PluginLifecycle>();
// HellionChat singletons. Factory lambdas because most
// classes are internal-sealed and the default activator only sees
// public ctors.
services.AddSingleton<IPlatformUtil>(_ => new DalamudPlatformUtil());
services.AddSingleton<IPluginLogProxy>(sp => new DalamudPluginLogProxy(
sp.GetRequiredService<IPluginLog>()
));
services.AddSingleton<FileDialogManager>(_ => new FileDialogManager());
services.AddSingleton(sp => new Commands(sp.GetRequiredService<ILogger<Commands>>()));
services.AddSingleton(sp => new FontManager(
sp.GetRequiredService<IDalamudPluginInterface>()
));
services.AddSingleton(sp => new IpcManager(sp.GetRequiredService<ILogger<IpcManager>>()));
services.AddSingleton(sp => new ExtraChat(sp.GetRequiredService<ILogger<ExtraChat>>()));
services.AddSingleton(sp => new ThemeRegistry(
Path.Combine(
sp.GetRequiredService<IDalamudPluginInterface>().ConfigDirectory.FullName,
"themes"
),
sp.GetRequiredService<ILogger<ThemeRegistry>>()
));
services.AddSingleton(_ => new Ui.StyleEngine.TokenResolver());
// Transient: each surface owns its motes, so two backdrops on screen do
// not drift in lockstep.
services.AddSingleton(sp => new Ui.Components.Settings.SectionRenderer(
sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
));
services.AddTransient(sp => new Ui.StyleEngine.SurfaceBackdrop(
sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
));
services.AddSingleton(sp => new Ui.StyleEngine.PushStack(
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
));
services.AddSingleton(sp => new GameFunctions.GameFunctions(
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<ILogger<GameFunctions.GameFunctions>>(),
sp.GetRequiredService<ILoggerFactory>()
));
services.AddSingleton(sp => new TypingIpc(
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<Ui.Components.InputBar>(),
sp.GetRequiredService<ILogger<TypingIpc>>()
));
services.AddSingleton(sp => new Integrations.HonorificService(
sp.GetRequiredService<IDalamudPluginInterface>(),
sp.GetRequiredService<ILogger<Integrations.HonorificService>>(),
sp.GetRequiredService<IFramework>()
));
services.AddSingleton(sp => new Services.TellRouterService(
sp.GetRequiredService<MessageManager>(),
sp.GetRequiredService<ILogger<Services.TellRouterService>>()
));
services.AddSingleton(sp => new Ui.Components.HonorificHeader(
sp.GetRequiredService<Integrations.HonorificService>(),
sp.GetRequiredService<FontManager>(),
sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
));
services.AddSingleton(sp => new Ui.Components.Sidebar(
sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>(),
sp.GetRequiredService<FontManager>(),
sp.GetRequiredService<ILogger<Ui.Components.Sidebar>>(),
sp.GetRequiredService<Ui.Windows.ChannelPopoutPool>()
));
services.AddSingleton(sp => new Ui.Components.MessageList(
sp.GetRequiredService<FontManager>(),
sp.GetRequiredService<Ui.Components.ChunkRenderer>()
));
services.AddSingleton(_ => new Ui.Components.SymbolPicker());
services.AddSingleton(sp => new Ui.Components.ThemeQuickPicker(
sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<Plugin>()
));
services.AddSingleton(sp => new Ui.Components.InputBar(
sp.GetRequiredService<Ui.Components.SymbolPicker>(),
sp.GetRequiredService<FontManager>(),
sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>(),
sp.GetRequiredService<ILogger<Ui.Components.InputBar>>(),
() => sp.GetRequiredService<Plugin>().SettingsWindow.Toggle(),
sp.GetRequiredService<Ui.CommandHelpWindow>(),
sp.GetRequiredService<Ui.Components.ThemeQuickPicker>(),
() => sp.GetRequiredService<Plugin>().MainWindow.UserHide()
));
services.AddSingleton(sp => new Ui.Components.Settings.TabSidebar(
sp.GetRequiredService<FontManager>(),
sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
));
services.AddSingleton(sp => new Ui.Components.Settings.ContentArea(
sp.GetRequiredService<Ui.StyleEngine.SurfaceBackdrop>()
));
services.AddSingleton(sp => new Ui.Components.Settings.ThemePicker(
sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<Ui.Components.Settings.SectionRenderer>()
));
services.AddSingleton(sp => new Ui.Components.Settings.ColorPicker(
sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<Ui.Components.Settings.SectionRenderer>()
));
services.AddSingleton(sp => new Ui.Components.Settings.LivePreviewPanel(
sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>(),
sp.GetRequiredService<FontManager>()
));
services.AddSingleton(sp => new Ui.Components.Settings.ThemeImportExportRow(
sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<ILogger<Ui.Components.Settings.ThemeImportExportRow>>()
));
services.AddSingleton(sp => new Ui.Components.Settings.FontsSection(
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<FontManager>(),
sp.GetRequiredService<Ui.Components.Settings.SectionRenderer>()
));
services.AddSingleton(sp => new Ui.Components.Settings.ChatColourPicker(
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<Ui.Components.Settings.SectionRenderer>()
));
services.AddSingleton(sp => new Ui.Components.Settings.Tabs.AppearanceTab(
sp.GetRequiredService<Ui.Components.Settings.ThemePicker>(),
sp.GetRequiredService<Ui.Components.Settings.ColorPicker>(),
sp.GetRequiredService<Ui.Components.Settings.LivePreviewPanel>(),
sp.GetRequiredService<Ui.Components.Settings.ThemeImportExportRow>(),
sp.GetRequiredService<Ui.Components.Settings.FontsSection>(),
sp.GetRequiredService<Ui.Components.Settings.ChatColourPicker>()
));
services.AddSingleton(sp => new Ui.Components.Settings.Tabs.GeneralTab(
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<FontManager>(),
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
));
services.AddSingleton(sp => new Ui.Components.Settings.Tabs.ChatTab(
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
));
services.AddSingleton(sp => new Ui.Components.Settings.Tabs.WindowTab(
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
));
services.AddSingleton(sp => new Ui.Components.Settings.Tabs.ChannelsTab(
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
));
services.AddSingleton(sp => new Ui.Components.Settings.Tabs.DataPrivacyTab(
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>(),
sp.GetRequiredService<ILogger<Ui.Components.Settings.Tabs.DataPrivacyTab>>()
));
services.AddSingleton(sp => new Ui.Components.Settings.Tabs.AboutTab(
sp.GetRequiredService<FontManager>(),
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<Integrations.HonorificService>(),
sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<IPlatformUtil>()
));
services.AddSingleton(sp => new Ui.Components.StatusBar(
sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<FontManager>()
));
services.AddSingleton(sp => new Ui.Components.TopTabBar(
sp.GetRequiredService<Ui.Windows.ChannelPopoutPool>(),
sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
));
services.AddSingleton(sp => new Ui.Windows.MainWindow(
sp.GetRequiredService<Ui.Components.HonorificHeader>(),
sp.GetRequiredService<Ui.Components.Sidebar>(),
sp.GetRequiredService<Ui.Components.TopTabBar>(),
sp.GetRequiredService<Ui.Components.MessageList>(),
sp.GetRequiredService<Ui.Components.InputBar>(),
sp.GetRequiredService<Ui.Components.StatusBar>(),
sp.GetRequiredService<Lender<PayloadHandler>>(),
sp.GetRequiredService<Ui.Windows.ChannelPopoutPool>(),
sp.GetRequiredService<Ui.StyleEngine.SurfaceBackdrop>()
));
services.AddSingleton(sp => new Integrations.FailedTellNotifier(
sp.GetRequiredService<ILogger<Integrations.FailedTellNotifier>>()
));
services.AddSingleton(sp => new Integrations.CustomAudioPlayer(
sp.GetRequiredService<ILogger<Integrations.CustomAudioPlayer>>()
));
services.AddSingleton(sp => new MessageManager(
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<ILogger<MessageManager>>(),
sp.GetRequiredService<ILoggerFactory>()
));
// MessageStore is allocated inside MessageManager.ctor; a separate
// container singleton would double-construct the SQLite handle.
services.AddSingleton(sp =>
{
var pluginRef = sp.GetRequiredService<Plugin>();
var manager = sp.GetRequiredService<MessageManager>();
return new AutoTellTabsService(
pluginRef,
manager,
manager.Store,
sp.GetRequiredService<ILogger<AutoTellTabsService>>()
);
});
// Factory-lambdas for ChunkRenderer, PayloadHandler, and Lender<PayloadHandler>
// because all three are internal-sealed (ActivatorUtilities can't reflect into
// internal ctors) and Lender<T> has an internal ctor by design.
// PayloadHandler registered twice: once as singleton for G/H, once via Lender<T> for per-frame isolation (I/J/K).
services.AddSingleton(sp => new Ui.Components.ChunkRenderer(
sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<FontManager>(),
sp.GetRequiredService<ILogger<Ui.Components.ChunkRenderer>>(),
sp.GetRequiredService<GameFunctions.GameFunctions>()
));
services.AddSingleton(sp => MakePayloadHandler(sp));
services.AddSingleton(sp => new Lender<PayloadHandler>(() => MakePayloadHandler(sp)));
// 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
// (would close a silent FactoryCallSite cycle).
services.AddSingleton<Func<int, Ui.Windows.ChannelPopoutWindow>>(sp =>
slot => new Ui.Windows.ChannelPopoutWindow(
slot,
new Ui.Components.MessageList(
sp.GetRequiredService<FontManager>(),
sp.GetRequiredService<Ui.Components.ChunkRenderer>()
),
new Ui.Components.InputBar(
sp.GetRequiredService<Ui.Components.SymbolPicker>(),
sp.GetRequiredService<FontManager>(),
sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>(),
sp.GetRequiredService<ILogger<Ui.Components.InputBar>>(),
() => sp.GetRequiredService<Plugin>().SettingsWindow.Toggle(),
sp.GetRequiredService<Ui.CommandHelpWindow>()
),
sp.GetRequiredService<ILogger<Ui.Windows.ChannelPopoutWindow>>(),
sp.GetRequiredService<FontManager>(),
sp.GetRequiredService<Ui.StyleEngine.SurfaceBackdrop>(),
sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
)
);
services.AddSingleton(sp => new Ui.Windows.ChannelPopoutPool(
sp.GetRequiredService<Func<int, Ui.Windows.ChannelPopoutWindow>>(),
sp.GetRequiredService<ILogger<Ui.Windows.ChannelPopoutPool>>()
));
// Windows. WindowSystem.AddWindow is called from
// PluginLifecycle.LoadAsync on the framework thread.
services.AddSingleton(sp => new Ui.Windows.SettingsWindow(
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<Ui.Components.Settings.TabSidebar>(),
sp.GetRequiredService<Ui.Components.Settings.ContentArea>(),
sp.GetRequiredService<Ui.Components.Settings.ThemePicker>(),
sp.GetRequiredService<Ui.Components.Settings.ColorPicker>(),
sp.GetRequiredService<Ui.Components.Settings.LivePreviewPanel>(),
sp.GetRequiredService<Ui.Components.Settings.Tabs.AppearanceTab>(),
sp.GetRequiredService<Ui.Components.Settings.Tabs.GeneralTab>(),
sp.GetRequiredService<Ui.Components.Settings.Tabs.ChatTab>(),
sp.GetRequiredService<Ui.Components.Settings.Tabs.WindowTab>(),
sp.GetRequiredService<Ui.Components.Settings.Tabs.ChannelsTab>(),
sp.GetRequiredService<Ui.Components.Settings.Tabs.DataPrivacyTab>(),
sp.GetRequiredService<Ui.Components.Settings.Tabs.AboutTab>(),
sp.GetRequiredService<ILoggerFactory>()
));
services.AddSingleton(sp => new DbViewer(
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<ILogger<DbViewer>>()
));
services.AddSingleton(sp => new InputPreview(
sp.GetRequiredService<Ui.Components.ChunkRenderer>(),
sp.GetRequiredService<Lender<PayloadHandler>>(),
sp.GetRequiredService<Ui.Windows.MainWindow>(),
sp.GetRequiredService<Ui.Components.InputBar>(),
sp.GetRequiredService<ILogger<InputPreview>>()
));
// No MainWindow ctor-param: breaks the InputBar -> CommandHelpWindow ->
// MainWindow -> InputBar singleton cycle. MainWindow is wired post-build
// via CommandHelpWindowInitHostedService.
services.AddSingleton(sp => new CommandHelpWindow(
sp.GetRequiredService<Ui.Components.ChunkRenderer>(),
sp.GetRequiredService<ILogger<CommandHelpWindow>>()
));
services.AddSingleton(sp => new SeStringDebugger(sp.GetRequiredService<Plugin>()));
#if DEBUG
services.AddSingleton(sp => new Ui.Windows.WidgetGalleryWindow(
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
));
#endif
// The style lab: variants side by side, in-game, against the live theme.
// 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>()
));
services.AddSingleton(sp => new DebuggerWindow(
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<PayloadHandler>()
));
services.AddSingleton(sp => new FirstRunWizard(
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<Ui.StyleEngine.SurfaceBackdrop>()
));
// Hosted-service adapters: thin wrappers around the existing init
// methods so the service class bodies stay unchanged. FontManager
// does not need one — its ctor runs the init inline inside a single
// SuppressAutoRebuild block on eager resolve.
services.AddHostedService(sp => new ThemeRegistryInitHostedService(
sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<FontManager>()
));
services.AddHostedService(sp => new IpcManagerInitHostedService(
sp.GetRequiredService<IpcManager>()
));
services.AddHostedService(sp => new TypingIpcInitHostedService(
sp.GetRequiredService<TypingIpc>()
));
services.AddHostedService(sp => new ExtraChatInitHostedService(
sp.GetRequiredService<ExtraChat>()
));
services.AddHostedService(sp => new MessageManagerInitHostedService(
sp.GetRequiredService<IDalamudPluginInterface>(),
sp.GetRequiredService<MessageManager>()
));
services.AddHostedService(sp => new AutoTellTabsServiceInitHostedService(
sp.GetRequiredService<AutoTellTabsService>()
));
// Must come AFTER AutoTell's registration: both subscribe MessageProcessed,
// and AutoTell subscribing first lets the router's IsOpen-guard see the
// already-opened pop-out (FIFO framework-tick ordering, no double-pop).
services.AddHostedService(sp => new TellRouterServiceInitHostedService(
sp.GetRequiredService<Services.TellRouterService>()
));
services.AddHostedService(
sp => new Infrastructure.Hosting.FailedTellNotifierInitHostedService(
sp.GetRequiredService<Integrations.FailedTellNotifier>()
)
);
services.AddHostedService(sp => new PayloadHandlerInitHostedService(
sp.GetRequiredService<PayloadHandler>(),
sp.GetRequiredService<Ui.Components.MessageList>()
));
services.AddHostedService(sp => new CommandHelpWindowInitHostedService(
sp.GetRequiredService<Ui.CommandHelpWindow>(),
sp.GetRequiredService<Ui.Windows.MainWindow>()
));
services.AddHostedService(sp => new ChannelPopoutInitHostedService(
sp.GetRequiredService<Ui.Windows.ChannelPopoutPool>(),
sp.GetRequiredService<PayloadHandler>()
));
}
private static PayloadHandler MakePayloadHandler(IServiceProvider sp) =>
new(
sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<IpcManager>(),
sp.GetRequiredService<GameFunctions.GameFunctions>(),
sp.GetRequiredService<Ui.Components.InputBar>(),
sp.GetRequiredService<Ui.Windows.MainWindow>(),
sp.GetRequiredService<Ui.Components.ChunkRenderer>(),
sp.GetRequiredService<ILogger<PayloadHandler>>()
);
}
internal sealed record PluginHostDependencies(
IDalamudPluginInterface PluginInterface,
IPluginLog PluginLog,
IChatGui ChatGui,
IClientState ClientState,
ICommandManager CommandManager,
ICondition Condition,
IDataManager DataManager,
IFramework Framework,
IGameGui GameGui,
IKeyState KeyState,
IObjectTable ObjectTable,
IPartyList PartyList,
ITargetManager TargetManager,
ITextureProvider TextureProvider,
IGameInteropProvider GameInteropProvider,
IGameConfig GameConfig,
INotificationManager Notification,
IAddonLifecycle AddonLifecycle,
IPlayerState PlayerState,
ISeStringEvaluator Evaluator,
ISelfTestRegistry SelfTestRegistry
);