diff --git a/HellionChat/Infrastructure/Hosting/InitHostedServices.cs b/HellionChat/Infrastructure/Hosting/InitHostedServices.cs index 43d1a81..49ed022 100644 --- a/HellionChat/Infrastructure/Hosting/InitHostedServices.cs +++ b/HellionChat/Infrastructure/Hosting/InitHostedServices.cs @@ -3,7 +3,9 @@ using Dalamud.Plugin; using HellionChat.Integrations; using HellionChat.Ipc; using HellionChat.Themes; +using HellionChat.Ui; using HellionChat.Ui.Components; +using HellionChat.Ui.Windows; using Microsoft.Extensions.Hosting; namespace HellionChat.Infrastructure.Hosting; @@ -142,3 +144,23 @@ internal sealed class PayloadHandlerInitHostedService( }); } } + +// Wires MainWindow into CommandHelpWindow post-container-build. CommandHelpWindow +// cannot take MainWindow as a ctor-param because that would close the cycle +// InputBar -> CommandHelpWindow -> MainWindow -> InputBar (MS.DI does not catch +// it through FactoryCallSite registrations and the resolve recurses silently). +// Both singletons exist by host.StartAsync time, so this is the first safe point +// to wire the setter — same §6.2 pattern as MessageList.AttachPayloadHandler. +internal sealed class CommandHelpWindowInitHostedService( + CommandHelpWindow commandHelpWindow, + MainWindow mainWindow +) : IHostedService +{ + public Task StartAsync(CancellationToken cancellationToken) + { + commandHelpWindow.AttachMainWindow(mainWindow); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; +} diff --git a/HellionChat/PluginHostFactory.cs b/HellionChat/PluginHostFactory.cs index ea3cdb7..b0dbb14 100644 --- a/HellionChat/PluginHostFactory.cs +++ b/HellionChat/PluginHostFactory.cs @@ -29,6 +29,15 @@ internal static class PluginHostFactory 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(); } @@ -268,9 +277,11 @@ internal static class PluginHostFactory sp.GetRequiredService(), sp.GetRequiredService>() )); + // 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(), - sp.GetRequiredService(), sp.GetRequiredService>() )); services.AddSingleton(sp => new SeStringDebugger(sp.GetRequiredService())); @@ -312,6 +323,10 @@ internal static class PluginHostFactory sp.GetRequiredService(), sp.GetRequiredService() )); + services.AddHostedService(sp => new CommandHelpWindowInitHostedService( + sp.GetRequiredService(), + sp.GetRequiredService() + )); } private static PayloadHandler MakePayloadHandler(IServiceProvider sp) => diff --git a/HellionChat/Ui/CommandHelpWindow.cs b/HellionChat/Ui/CommandHelpWindow.cs index 53ee9c8..0d4caa4 100644 --- a/HellionChat/Ui/CommandHelpWindow.cs +++ b/HellionChat/Ui/CommandHelpWindow.cs @@ -13,20 +13,21 @@ namespace HellionChat.Ui; internal sealed class CommandHelpWindow : Window { private readonly ChunkRenderer _chunkRenderer; - private readonly Windows.MainWindow _mainWindow; private readonly ILogger _logger; + // Setter-injected post-ctor to break the InputBar -> CommandHelpWindow -> + // MainWindow -> InputBar singleton cycle (MS.DI does not detect cycles + // through FactoryCallSite registrations). Wired in + // CommandHelpWindowInitHostedService.StartAsync, same §6.2 pattern as + // MessageList.AttachPayloadHandler. + private Windows.MainWindow? _mainWindow; + private ReadOnlySeString? _commandDescription; - internal CommandHelpWindow( - ChunkRenderer chunkRenderer, - Windows.MainWindow mainWindow, - ILogger logger - ) + internal CommandHelpWindow(ChunkRenderer chunkRenderer, ILogger logger) : base("command help##chat2-commandhelp") { _chunkRenderer = chunkRenderer; - _mainWindow = mainWindow; _logger = logger; Flags = @@ -44,8 +45,18 @@ internal sealed class CommandHelpWindow : Window _ = _logger; } + internal void AttachMainWindow(Windows.MainWindow mainWindow) => _mainWindow = mainWindow; + public void UpdateContent(ReadOnlySeString commandDesc) { + // Loud-fail if the HostedService didn't run AttachMainWindow before + // the first slash-command call — better than a silent NullRef during + // input draw. + if (_mainWindow is null) + throw new InvalidOperationException( + "CommandHelpWindow.UpdateContent called before AttachMainWindow." + ); + _commandDescription = commandDesc; var width = 350;