Files
HellionChat/HellionChat/PluginHostFactory.cs
T
JonKazama-Hellion 76416afe3a feat(style): depth for the settings window
Brief was a mix of Lightless and Character Select+, modern without being
overloaded, and the gradient allowed to show more.

The content pane gets a stronger gradient plus an accent wash falling from the
top edge to about a third of the height. Without it the pane was a neutral box
that happened to sit inside a themed window; now the theme reaches the surface
being read.

Ambient motes drift up behind it. Procedural rather than the image sequence
Character Select+ uses, so there are no assets to ship and they take the theme's
accent instead of whatever was baked into a PNG. Fixed count, fixed arrays,
seeded once, so the layout is identical every session and nothing allocates per
frame. They advance once per frame regardless of how many surfaces call in, and
ReduceMotion skips them entirely -- same contract HoverState already honours.

The tab sidebar was the last stock-ImGui element and had started to look it. Its
entries are drawn now: darker plane than the content pane so the two read as
separate surfaces, a chamfered plate with a solid leading bar for the active
one, hover fading between muted and full text.

Its icons go through the draw list with a font pointer taken outside the scope,
rather than being drawn inside a FontAwesome push. That atlas has no ASCII
glyphs, so any label caught inside such a scope renders blank -- which has
already happened twice in this plugin, once in the tooltips and once in a badge.
2026-08-18 17:50:32 +02:00

448 lines
22 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. See plan §1 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
)
{
// Block A — 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>();
// Block B — 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());
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<ThemeRegistry>(),
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
));
services.AddSingleton(sp => new Ui.Components.Settings.ThemePicker(
sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<Plugin>()
));
services.AddSingleton(sp => new Ui.Components.Settings.ColorPicker(
sp.GetRequiredService<ThemeRegistry>()
));
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>()
));
services.AddSingleton(sp => new Ui.Components.Settings.ChatColourPicker(
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<ThemeRegistry>()
));
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>()
));
services.AddSingleton(sp => new Ui.Components.Settings.Tabs.ChatTab(
sp.GetRequiredService<Plugin>()
));
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>()
));
services.AddSingleton(sp => new Ui.Components.Settings.Tabs.DataPrivacyTab(
sp.GetRequiredService<Plugin>()
));
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>()
));
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
// (plan §B.2 — 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>()
)
);
services.AddSingleton(sp => new Ui.Windows.ChannelPopoutPool(
sp.GetRequiredService<Func<int, Ui.Windows.ChannelPopoutWindow>>(),
sp.GetRequiredService<ILogger<Ui.Windows.ChannelPopoutPool>>()
));
// Block C — 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
services.AddSingleton(sp => new DebuggerWindow(
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<PayloadHandler>()
));
services.AddSingleton(sp => new FirstRunWizard(sp.GetRequiredService<Plugin>()));
// 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
);