diff --git a/HellionChat/FontManager.cs b/HellionChat/FontManager.cs index afb6f04..e0b7453 100644 --- a/HellionChat/FontManager.cs +++ b/HellionChat/FontManager.cs @@ -8,12 +8,9 @@ using Dalamud.Interface.Utility; namespace HellionChat; -// FontManager's two LogProxy sites both live in static methods -// (TryGetHellionFontBytes, AddFontWithFallback) that the BuildFonts pipeline -// invokes; an instance _logger field would be unreachable from those scopes. -// DI-4 Slice D leaves the class on Plugin.LogProxy and counts it under the -// "static consumers" bucket alongside EmoteCache / AutoTranslate / -// MemoryUtil / WrapperUtil. +// Two LogProxy sites live in static methods (TryGetHellionFontBytes, +// AddFontWithFallback); a ctor-injected ILogger would not be reachable +// from those scopes, so the class stays on Plugin.LogProxy. public class FontManager { internal IFontHandle Axis = null!; @@ -64,9 +61,6 @@ public class FontManager ); if (stream is null) { - // Static method has no instance _logger to reach. The resource- - // missing path is rare (only fires when the embedded font is - // stripped from the build), so Plugin.LogProxy is acceptable. Plugin.LogProxy.Warning( "Hellion font resource missing — falling back to system default font." ); @@ -243,11 +237,9 @@ public class FontManager or ArgumentException ) { - // Atlas-toolkit throws span IO and validation failures; routing the - // wider set through the fallback keeps a corrupt font config from - // taking down the whole atlas build. Static method has no instance - // _logger to reach (Plugin.Config-driven font swap, called from - // BuildFonts). + // Atlas-toolkit throws span IO and validation failures; routing + // the wider set through the fallback keeps a corrupt font config + // from taking down the whole atlas build. Plugin.LogProxy.Warning( e, $"Configured {slot} font failed to load ({e.GetType().Name}), " diff --git a/HellionChat/GameFunctions/GameFunctions.cs b/HellionChat/GameFunctions/GameFunctions.cs index 5e48942..c807760 100755 --- a/HellionChat/GameFunctions/GameFunctions.cs +++ b/HellionChat/GameFunctions/GameFunctions.cs @@ -222,10 +222,7 @@ internal unsafe class GameFunctions : IDisposable } catch (Exception e) { - // Static method has no instance _logger to reach. Promoting this to - // an instance method would force PayloadHandler.cs:814 (the only - // caller) onto Plugin.Functions.* indirection. Lighter touch for - // DI-4 Slice B is to keep this one site on Plugin.LogProxy. + // Static method, no instance _logger reachable here. Plugin.LogProxy.Warning(e, "Unable to open adventurer plate"); return false; } diff --git a/HellionChat/HellionChat.csproj b/HellionChat/HellionChat.csproj index 51f6e40..e2dd1cb 100644 --- a/HellionChat/HellionChat.csproj +++ b/HellionChat/HellionChat.csproj @@ -16,7 +16,10 @@ - + diff --git a/HellionChat/Infrastructure/Hosting/InitHostedServices.cs b/HellionChat/Infrastructure/Hosting/InitHostedServices.cs index d7932e5..e43f482 100644 --- a/HellionChat/Infrastructure/Hosting/InitHostedServices.cs +++ b/HellionChat/Infrastructure/Hosting/InitHostedServices.cs @@ -5,13 +5,11 @@ using Microsoft.Extensions.Hosting; namespace HellionChat.Infrastructure.Hosting; -// Adapter shells around the IHostedService contract so the host can resolve -// the underlying singletons eagerly and trigger their existing init methods -// without modifying the service class bodies (DI-2a constraint). Bodies that -// stay empty here still serve a purpose: the host resolves the service when -// it instantiates the hosted service, which forces the ctor (IPC subscribe -// for IpcManager / TypingIpc / ExtraChat) to run during StartAsync instead of -// lazily on first GetRequiredService. +// Adapter shells around IHostedService so the host triggers each service's +// existing init method without touching the service class itself. Empty +// adapters still earn their place: registering them forces an eager resolve +// at Build, which runs the service ctor (IPC subscribe etc.) right then +// instead of lazily on first GetRequiredService. internal sealed class FontManagerInitHostedService(FontManager fontManager) : IHostedService { @@ -39,11 +37,8 @@ internal sealed class ThemeRegistryInitHostedService(ThemeRegistry registry) : I public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; } -// IPC subscribers do their wiring in the ctor today, so StartAsync stays a -// no-op — the value of registering them as hosted services is that the host -// resolves them eagerly during Build, which triggers the ctor work. Moving -// the body into StartAsync is a DI-2b follow-up after the service ctors are -// allowed to change. +// IPC subscribers do their wiring in the ctor, so StartAsync stays empty — +// the registration alone forces an eager resolve which runs that wiring. internal sealed class IpcManagerInitHostedService(IpcManager ipc) : IHostedService { diff --git a/HellionChat/Infrastructure/Logging/DalamudLogger.cs b/HellionChat/Infrastructure/Logging/DalamudLogger.cs index 09375ff..0bb8462 100644 --- a/HellionChat/Infrastructure/Logging/DalamudLogger.cs +++ b/HellionChat/Infrastructure/Logging/DalamudLogger.cs @@ -33,11 +33,8 @@ internal sealed class DalamudLogger : ILogger if (!IsEnabled(logLevel)) return; - // The U+200B zero-width space between the bracket and the level - // value is a quiet provenance marker. The Hellion DI-Logger format - // is byte-distinguishable from any other port of this pattern even - // after the visible text is identical. EUPL-1.2 reuse stays valid; - // attribution traces stay possible. + // U+200B between the bracket and the level is a quiet provenance + // marker; byte-distinguishable from any 1:1 port of this format. if ((int)logLevel <= (int)LogLevel.Information) { _pluginLog.Information($"[{_name}]​{{{(int)logLevel}}} {state}"); diff --git a/HellionChat/Infrastructure/Logging/DalamudLoggingProvider.cs b/HellionChat/Infrastructure/Logging/DalamudLoggingProvider.cs index befb215..ad948cb 100644 --- a/HellionChat/Infrastructure/Logging/DalamudLoggingProvider.cs +++ b/HellionChat/Infrastructure/Logging/DalamudLoggingProvider.cs @@ -10,9 +10,7 @@ namespace HellionChat.Infrastructure.Logging; [ProviderAlias("Dalamud")] public sealed class DalamudLoggingProvider : ILoggerProvider { - // Hellion Forge Bronze (#C2410C). Stable marker that the build pipeline - // never touches; mixed into the bootstrap fingerprint so the banner stays - // distinguishable from any 1:1 port of the Lightless pattern. + // Hellion Forge Bronze (#C2410C). Mixed into the bootstrap fingerprint. private const string HellionMarker = "HellionForgeBronzeC2410C"; private readonly ConcurrentDictionary _loggers = new( @@ -27,12 +25,8 @@ public sealed class DalamudLoggingProvider : ILoggerProvider EmitBootstrapBanner(); } - // Runs once per plugin load (the provider is a container singleton). The - // banner is intentionally visible in xllog: anyone copying the - // DalamudLogger trio without re-branding will keep emitting "HellionChat - // DI-Logger bootstrap …", which makes uncredited reuse trivial to spot. - // EUPL-1.2 reuse with attribution stays valid; this only catches the - // case where attribution was stripped. + // One-shot per plugin load. Intentionally visible in xllog so uncredited + // ports of the DalamudLogger trio keep announcing their origin. private void EmitBootstrapBanner() { var version = diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index c374464..31ceff3 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -125,9 +125,8 @@ public sealed class Plugin : IAsyncDalamudPlugin // isolation. Wired immediately after Dalamud injects Log. internal static IPluginLogProxy LogProxy { get; private set; } = null!; - // Container drives the v1.5.0 bootstrap. Both are nullable so DisposeAsync - // stays safe if Phase-1 (Host build) throws before they get assigned - - // Dalamud fires DisposeAsync regardless of how far the ctor got. + // Nullable so DisposeAsync stays safe if Host-build throws before the + // fields get assigned — Dalamud fires DisposeAsync regardless. private readonly IHost? _host; private readonly PluginLifecycle? _lifecycle; @@ -222,17 +221,14 @@ public sealed class Plugin : IAsyncDalamudPlugin DeferredSaveFrames = -1; // Custom themes dir + seed run before the container builds so the - // ThemeRegistry factory lambda finds the directory ready and the - // example theme stays in place if the user has not touched it. + // ThemeRegistry factory lambda finds the directory ready. var customThemesDir = Path.Combine(Interface.ConfigDirectory.FullName, "themes"); Directory.CreateDirectory(customThemesDir); SeedExampleThemeIfEmpty(customThemesDir); - // Phase-1: build the generic host and pull singletons out into the - // Plugin.X surface so consumers untouched by DI-2a keep working. The - // host stays sync here because the schema gate above must run before - // services allocate; deferring the build to LoadAsync (Lightless' - // pattern) would mean the gate fires after the container is alive. + // Phase-1: build the host synchronously (the schema gate must clear + // before services allocate; Lightless' deferred build would invert + // that order) and pull singletons into the Plugin.X surface. var dependencies = new PluginHostDependencies( Interface, Log, @@ -527,10 +523,7 @@ public sealed class Plugin : IAsyncDalamudPlugin } ); - // Framework-thread cleanup the container does not reach. TearDownCommands - // walks Plugin-private dictionaries; SetChatInteractable is a static - // call into game state; WindowSystem.RemoveAllWindows clears the - // backing List<> that AddWindow populated in PluginLifecycle.LoadAsync. + // Framework-thread cleanup the container does not reach. try { await Framework @@ -550,12 +543,9 @@ public sealed class Plugin : IAsyncDalamudPlugin failure ??= ex; } - // Lifecycle stops the host (HostedService.StopAsync) and disposes the - // container on the framework thread; that path disposes all the - // services + windows we used to dispose manually here. The smoke from - // C3 surfaced MessageManager.DisposeAsync as non-idempotent (CTS - // dispose at line 99 throws on a second call), so we hand the entire - // service teardown to the container instead of double-disposing. + // Container disposes services + windows on the framework thread. + // MessageManager.DisposeAsync is not idempotent, so we let the + // container do it once instead of double-disposing. if (_lifecycle is not null) { failure = await CaptureFailureAsync(failure, () => _lifecycle.DisposeAsync().AsTask()) diff --git a/HellionChat/PluginHostFactory.cs b/HellionChat/PluginHostFactory.cs index 32d5199..c8781bc 100644 --- a/HellionChat/PluginHostFactory.cs +++ b/HellionChat/PluginHostFactory.cs @@ -39,11 +39,7 @@ internal static class PluginHostFactory PluginHostDependencies dependencies ) { - // ----------------------------------------------------------------- - // Block A — Dalamud-Services (21 [PluginService] singletons, plus the - // dependencies record itself). Registered by-interface so consumer - // ctors can resolve them without touching Plugin statics. - // ----------------------------------------------------------------- + // Block A — Dalamud services (21 [PluginService] singletons). services.AddSingleton(dependencies); services.AddSingleton(dependencies.PluginInterface); services.AddSingleton(dependencies.PluginLog); @@ -67,31 +63,14 @@ internal static class PluginHostFactory services.AddSingleton(dependencies.Evaluator); services.AddSingleton(dependencies.SelfTestRegistry); - // ----------------------------------------------------------------- - // Self-reference. Plugin owns the [PluginService] static surface and - // is already constructed by Dalamud before this factory runs, so we - // register the existing instance instead of letting the container - // build one. - // ----------------------------------------------------------------- + // Self-references: Plugin and its WindowSystem already exist. services.AddSingleton(plugin); services.AddSingleton(plugin.WindowSystem); - - // PluginLifecycle is a thin orchestrator over IHost; Plugin.ctor pulls - // it via GetRequiredService() immediately after Build. services.AddSingleton(); - // ----------------------------------------------------------------- - // Block B — HellionChat singletons (14 + 1 FileDialogManager adapter). - // Service-class bodies stay untouched in v1.5.0 per the DI-2a - // constraint; ctors that need a Plugin backref go through a factory - // lambda that resolves Plugin from the container. - // ----------------------------------------------------------------- - // Factory lambdas across the board: Microsoft.Extensions.DependencyInjection's - // ActivatorUtilities only inspects PUBLIC constructors via reflection, - // and several HellionChat classes are `internal sealed` with implicit- - // internal default ctors (Commands, StatusBar) or explicitly `internal` - // ctors on public classes (ExtraChat). The lambda body compiles inside - // the HellionChat namespace, so `new T()` sees the internal surface. + // Block B — HellionChat singletons. Factory lambdas because most + // classes are internal-sealed and the default activator only sees + // public ctors. services.AddSingleton(_ => new DalamudPlatformUtil()); services.AddSingleton(sp => new DalamudPluginLogProxy( sp.GetRequiredService() @@ -133,10 +112,8 @@ internal static class PluginHostFactory sp.GetRequiredService() )); - // AutoTellTabsService pulls MessageStore through MessageManager.Store - // because MessageStore is still allocated inside MessageManager.ctor - // (DI-2a leaves that body untouched). Promoting MessageStore to its - // own container singleton would double-construct the SQLite handle. + // MessageStore is allocated inside MessageManager.ctor; a separate + // container singleton would double-construct the SQLite handle. services.AddSingleton(sp => { var pluginRef = sp.GetRequiredService(); @@ -149,11 +126,8 @@ internal static class PluginHostFactory ); }); - // ----------------------------------------------------------------- - // Block C — Windows (8, each takes Plugin or ChatLogWindow). The - // host never AddWindow()s them; PluginLifecycle does that on the - // framework thread once C3 wires it up (see plan §2 service order). - // ----------------------------------------------------------------- + // Block C — Windows. WindowSystem.AddWindow is called from + // PluginLifecycle.LoadAsync on the framework thread. services.AddSingleton(sp => new ChatLogWindow( sp.GetRequiredService(), sp.GetRequiredService>(), @@ -173,18 +147,8 @@ internal static class PluginHostFactory services.AddSingleton(sp => new DebuggerWindow(sp.GetRequiredService())); services.AddSingleton(sp => new FirstRunWizard(sp.GetRequiredService())); - // ----------------------------------------------------------------- - // Hosted-service adapters — IHostedService is the host's only "after - // bootstrap, before user interaction" hook, so we register thin - // wrappers that call the existing init methods (BuildFonts, Switch, - // FilterAllTabsAsync, Initialize) without modifying the service - // bodies. Plan §2 documents why this is adapter-style instead of - // making the services themselves implement IHostedService (Lightless' - // pattern) — DI-2a leaves service classes untouched. - // ----------------------------------------------------------------- - // Same internal-ctor pitfall as the singletons above - the adapter - // classes are `internal sealed` with primary constructors, so the - // direct AddHostedService() overload's ActivatorUtilities fails. + // Hosted-service adapters: thin wrappers around the existing init + // methods so the service class bodies stay unchanged. services.AddHostedService(sp => new FontManagerInitHostedService( sp.GetRequiredService() )); diff --git a/HellionChat/PluginLifecycle.cs b/HellionChat/PluginLifecycle.cs index 8497cbf..052fdf8 100644 --- a/HellionChat/PluginLifecycle.cs +++ b/HellionChat/PluginLifecycle.cs @@ -4,11 +4,9 @@ using Microsoft.Extensions.Hosting; namespace HellionChat; -// Orchestrates Host.StartAsync / StopAsync + dispose on the framework thread. -// The Host itself is built sync in Plugin.ctor (before the schema gate clears) -// and assigned via the property setter; PluginLifecycle never builds it -// itself, which is why HellionChat skips Lightless' Func-delegate indirection -// (see plan §9 risk "Bewusste Abweichung von Lightless"). +// Orchestrates Host.StartAsync / StopAsync and the framework-thread dispose. +// Plugin.ctor builds the host and assigns it via the Host property, so +// PluginLifecycle never constructs the host itself. internal sealed class PluginLifecycle : IAsyncDisposable { private readonly IFramework _framework; diff --git a/HellionChat/Util/IPluginLogProxy.cs b/HellionChat/Util/IPluginLogProxy.cs index 40fd4bc..f3a7d35 100644 --- a/HellionChat/Util/IPluginLogProxy.cs +++ b/HellionChat/Util/IPluginLogProxy.cs @@ -2,10 +2,10 @@ using System; namespace HellionChat.Util; -// Indirection over Dalamud's IPluginLog so MessageStore can be constructed -// in an isolated xUnit AppDomain without loading Dalamud.dll — same pattern -// as IPlatformUtil from F12.1. A later DI-container cycle (v1.5.x) may -// replace this with Microsoft.Extensions.Logging's ILogger. +// Plugin.LogProxy bridge for consumers that cannot take a logger via the +// constructor: static helpers (EmoteCache et al.), Dalamud-reflected types +// (Configuration), data classes with mass instantiation (Message) and +// instance classes that only log from static methods (FontManager). internal interface IPluginLogProxy { void Verbose(string message);