docs(di): trim cycle-internal codes and verbose block comments

Code comments were drifting into plan-internal shorthand (DI-2a,
Slice B, "see plan §9") that nobody outside the cycle authors can
decode. They also tended toward AI-generated paragraph blocks where a
two-line WHY would have done.

This commit tightens the comment surface from the v1.5.0 work:
- IPluginLogProxy header lists the consumer buckets without naming
  the cycle items that decided them.
- DalamudLogger / DalamudLoggingProvider provenance markers explain
  themselves in two lines each; the long EUPL-rationale paragraph
  moves to the commit message.
- PluginHostFactory block headers shrink to one line each, ASCII
  dividers come out, plan-internal codes go.
- Plugin.cs field doc and Phase-1 / DisposeAsync comments lose the
  cycle-name references; the file gains nothing from "C3 surfaced X"
  in code.
- FontManager / GameFunctions static-method notes shrink to one
  sentence each.
- InitHostedServices class header keeps the eager-resolve WHY in
  three lines, drops the constraint label.

Csharpier reformatted the .csproj layout (long PackageReference
multi-lined). No functional change, no behavior change.
This commit is contained in:
2026-05-17 11:35:44 +02:00
parent 624ad20404
commit fe84fd558e
10 changed files with 51 additions and 121 deletions
+6 -14
View File
@@ -8,12 +8,9 @@ using Dalamud.Interface.Utility;
namespace HellionChat; namespace HellionChat;
// FontManager's two LogProxy sites both live in static methods // Two LogProxy sites live in static methods (TryGetHellionFontBytes,
// (TryGetHellionFontBytes, AddFontWithFallback) that the BuildFonts pipeline // AddFontWithFallback); a ctor-injected ILogger would not be reachable
// invokes; an instance _logger field would be unreachable from those scopes. // from those scopes, so the class stays on Plugin.LogProxy.
// DI-4 Slice D leaves the class on Plugin.LogProxy and counts it under the
// "static consumers" bucket alongside EmoteCache / AutoTranslate /
// MemoryUtil / WrapperUtil.
public class FontManager public class FontManager
{ {
internal IFontHandle Axis = null!; internal IFontHandle Axis = null!;
@@ -64,9 +61,6 @@ public class FontManager
); );
if (stream is null) 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( Plugin.LogProxy.Warning(
"Hellion font resource missing — falling back to system default font." "Hellion font resource missing — falling back to system default font."
); );
@@ -243,11 +237,9 @@ public class FontManager
or ArgumentException or ArgumentException
) )
{ {
// Atlas-toolkit throws span IO and validation failures; routing the // Atlas-toolkit throws span IO and validation failures; routing
// wider set through the fallback keeps a corrupt font config from // the wider set through the fallback keeps a corrupt font config
// taking down the whole atlas build. Static method has no instance // from taking down the whole atlas build.
// _logger to reach (Plugin.Config-driven font swap, called from
// BuildFonts).
Plugin.LogProxy.Warning( Plugin.LogProxy.Warning(
e, e,
$"Configured {slot} font failed to load ({e.GetType().Name}), " $"Configured {slot} font failed to load ({e.GetType().Name}), "
+1 -4
View File
@@ -222,10 +222,7 @@ internal unsafe class GameFunctions : IDisposable
} }
catch (Exception e) catch (Exception e)
{ {
// Static method has no instance _logger to reach. Promoting this to // Static method, no instance _logger reachable here.
// 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.
Plugin.LogProxy.Warning(e, "Unable to open adventurer plate"); Plugin.LogProxy.Warning(e, "Unable to open adventurer plate");
return false; return false;
} }
+4 -1
View File
@@ -16,7 +16,10 @@
<PackageReference Include="MessagePack" Version="[3.1.4, 4.0.0)" /> <PackageReference Include="MessagePack" Version="[3.1.4, 4.0.0)" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.7" /> <PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.7" />
<!-- v1.5.0 DI-container foundation; matches Lightless pin (Hosting 10.0.7) --> <!-- v1.5.0 DI-container foundation; matches Lightless pin (Hosting 10.0.7) -->
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="[10.0.7, 11.0.0)" /> <PackageReference
Include="Microsoft.Extensions.DependencyInjection"
Version="[10.0.7, 11.0.0)"
/>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="[10.0.7, 11.0.0)" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="[10.0.7, 11.0.0)" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="[10.0.7, 11.0.0)" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="[10.0.7, 11.0.0)" />
<PackageReference Include="Microsoft.Extensions.Options" Version="[10.0.7, 11.0.0)" /> <PackageReference Include="Microsoft.Extensions.Options" Version="[10.0.7, 11.0.0)" />
@@ -5,13 +5,11 @@ using Microsoft.Extensions.Hosting;
namespace HellionChat.Infrastructure.Hosting; namespace HellionChat.Infrastructure.Hosting;
// Adapter shells around the IHostedService contract so the host can resolve // Adapter shells around IHostedService so the host triggers each service's
// the underlying singletons eagerly and trigger their existing init methods // existing init method without touching the service class itself. Empty
// without modifying the service class bodies (DI-2a constraint). Bodies that // adapters still earn their place: registering them forces an eager resolve
// stay empty here still serve a purpose: the host resolves the service when // at Build, which runs the service ctor (IPC subscribe etc.) right then
// it instantiates the hosted service, which forces the ctor (IPC subscribe // instead of lazily on first GetRequiredService.
// for IpcManager / TypingIpc / ExtraChat) to run during StartAsync instead of
// lazily on first GetRequiredService.
internal sealed class FontManagerInitHostedService(FontManager fontManager) : IHostedService 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; public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
} }
// IPC subscribers do their wiring in the ctor today, so StartAsync stays a // IPC subscribers do their wiring in the ctor, so StartAsync stays empty —
// no-op — the value of registering them as hosted services is that the host // the registration alone forces an eager resolve which runs that wiring.
// 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.
internal sealed class IpcManagerInitHostedService(IpcManager ipc) : IHostedService internal sealed class IpcManagerInitHostedService(IpcManager ipc) : IHostedService
{ {
@@ -33,11 +33,8 @@ internal sealed class DalamudLogger : ILogger
if (!IsEnabled(logLevel)) if (!IsEnabled(logLevel))
return; return;
// The U+200B zero-width space between the bracket and the level // U+200B between the bracket and the level is a quiet provenance
// value is a quiet provenance marker. The Hellion DI-Logger format // marker; byte-distinguishable from any 1:1 port of this 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.
if ((int)logLevel <= (int)LogLevel.Information) if ((int)logLevel <= (int)LogLevel.Information)
{ {
_pluginLog.Information($"[{_name}]{{{(int)logLevel}}} {state}"); _pluginLog.Information($"[{_name}]{{{(int)logLevel}}} {state}");
@@ -10,9 +10,7 @@ namespace HellionChat.Infrastructure.Logging;
[ProviderAlias("Dalamud")] [ProviderAlias("Dalamud")]
public sealed class DalamudLoggingProvider : ILoggerProvider public sealed class DalamudLoggingProvider : ILoggerProvider
{ {
// Hellion Forge Bronze (#C2410C). Stable marker that the build pipeline // Hellion Forge Bronze (#C2410C). Mixed into the bootstrap fingerprint.
// never touches; mixed into the bootstrap fingerprint so the banner stays
// distinguishable from any 1:1 port of the Lightless pattern.
private const string HellionMarker = "HellionForgeBronzeC2410C"; private const string HellionMarker = "HellionForgeBronzeC2410C";
private readonly ConcurrentDictionary<string, DalamudLogger> _loggers = new( private readonly ConcurrentDictionary<string, DalamudLogger> _loggers = new(
@@ -27,12 +25,8 @@ public sealed class DalamudLoggingProvider : ILoggerProvider
EmitBootstrapBanner(); EmitBootstrapBanner();
} }
// Runs once per plugin load (the provider is a container singleton). The // One-shot per plugin load. Intentionally visible in xllog so uncredited
// banner is intentionally visible in xllog: anyone copying the // ports of the DalamudLogger trio keep announcing their origin.
// 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.
private void EmitBootstrapBanner() private void EmitBootstrapBanner()
{ {
var version = var version =
+10 -20
View File
@@ -125,9 +125,8 @@ public sealed class Plugin : IAsyncDalamudPlugin
// isolation. Wired immediately after Dalamud injects Log. // isolation. Wired immediately after Dalamud injects Log.
internal static IPluginLogProxy LogProxy { get; private set; } = null!; internal static IPluginLogProxy LogProxy { get; private set; } = null!;
// Container drives the v1.5.0 bootstrap. Both are nullable so DisposeAsync // Nullable so DisposeAsync stays safe if Host-build throws before the
// stays safe if Phase-1 (Host build) throws before they get assigned - // fields get assigned — Dalamud fires DisposeAsync regardless.
// Dalamud fires DisposeAsync regardless of how far the ctor got.
private readonly IHost? _host; private readonly IHost? _host;
private readonly PluginLifecycle? _lifecycle; private readonly PluginLifecycle? _lifecycle;
@@ -222,17 +221,14 @@ public sealed class Plugin : IAsyncDalamudPlugin
DeferredSaveFrames = -1; DeferredSaveFrames = -1;
// Custom themes dir + seed run before the container builds so the // Custom themes dir + seed run before the container builds so the
// ThemeRegistry factory lambda finds the directory ready and the // ThemeRegistry factory lambda finds the directory ready.
// example theme stays in place if the user has not touched it.
var customThemesDir = Path.Combine(Interface.ConfigDirectory.FullName, "themes"); var customThemesDir = Path.Combine(Interface.ConfigDirectory.FullName, "themes");
Directory.CreateDirectory(customThemesDir); Directory.CreateDirectory(customThemesDir);
SeedExampleThemeIfEmpty(customThemesDir); SeedExampleThemeIfEmpty(customThemesDir);
// Phase-1: build the generic host and pull singletons out into the // Phase-1: build the host synchronously (the schema gate must clear
// Plugin.X surface so consumers untouched by DI-2a keep working. The // before services allocate; Lightless' deferred build would invert
// host stays sync here because the schema gate above must run before // that order) and pull singletons into the Plugin.X surface.
// services allocate; deferring the build to LoadAsync (Lightless'
// pattern) would mean the gate fires after the container is alive.
var dependencies = new PluginHostDependencies( var dependencies = new PluginHostDependencies(
Interface, Interface,
Log, Log,
@@ -527,10 +523,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
} }
); );
// Framework-thread cleanup the container does not reach. TearDownCommands // Framework-thread cleanup the container does not reach.
// walks Plugin-private dictionaries; SetChatInteractable is a static
// call into game state; WindowSystem.RemoveAllWindows clears the
// backing List<> that AddWindow populated in PluginLifecycle.LoadAsync.
try try
{ {
await Framework await Framework
@@ -550,12 +543,9 @@ public sealed class Plugin : IAsyncDalamudPlugin
failure ??= ex; failure ??= ex;
} }
// Lifecycle stops the host (HostedService.StopAsync) and disposes the // Container disposes services + windows on the framework thread.
// container on the framework thread; that path disposes all the // MessageManager.DisposeAsync is not idempotent, so we let the
// services + windows we used to dispose manually here. The smoke from // container do it once instead of double-disposing.
// 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.
if (_lifecycle is not null) if (_lifecycle is not null)
{ {
failure = await CaptureFailureAsync(failure, () => _lifecycle.DisposeAsync().AsTask()) failure = await CaptureFailureAsync(failure, () => _lifecycle.DisposeAsync().AsTask())
+11 -47
View File
@@ -39,11 +39,7 @@ internal static class PluginHostFactory
PluginHostDependencies dependencies PluginHostDependencies dependencies
) )
{ {
// ----------------------------------------------------------------- // Block A — Dalamud services (21 [PluginService] singletons).
// 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.
// -----------------------------------------------------------------
services.AddSingleton(dependencies); services.AddSingleton(dependencies);
services.AddSingleton(dependencies.PluginInterface); services.AddSingleton(dependencies.PluginInterface);
services.AddSingleton(dependencies.PluginLog); services.AddSingleton(dependencies.PluginLog);
@@ -67,31 +63,14 @@ internal static class PluginHostFactory
services.AddSingleton(dependencies.Evaluator); services.AddSingleton(dependencies.Evaluator);
services.AddSingleton(dependencies.SelfTestRegistry); services.AddSingleton(dependencies.SelfTestRegistry);
// ----------------------------------------------------------------- // Self-references: Plugin and its WindowSystem already exist.
// 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.
// -----------------------------------------------------------------
services.AddSingleton(plugin); services.AddSingleton(plugin);
services.AddSingleton(plugin.WindowSystem); services.AddSingleton(plugin.WindowSystem);
// PluginLifecycle is a thin orchestrator over IHost; Plugin.ctor pulls
// it via GetRequiredService<PluginLifecycle>() immediately after Build.
services.AddSingleton<PluginLifecycle>(); services.AddSingleton<PluginLifecycle>();
// ----------------------------------------------------------------- // Block B — HellionChat singletons. Factory lambdas because most
// Block B — HellionChat singletons (14 + 1 FileDialogManager adapter). // classes are internal-sealed and the default activator only sees
// Service-class bodies stay untouched in v1.5.0 per the DI-2a // public ctors.
// 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.
services.AddSingleton<IPlatformUtil>(_ => new DalamudPlatformUtil()); services.AddSingleton<IPlatformUtil>(_ => new DalamudPlatformUtil());
services.AddSingleton<IPluginLogProxy>(sp => new DalamudPluginLogProxy( services.AddSingleton<IPluginLogProxy>(sp => new DalamudPluginLogProxy(
sp.GetRequiredService<IPluginLog>() sp.GetRequiredService<IPluginLog>()
@@ -133,10 +112,8 @@ internal static class PluginHostFactory
sp.GetRequiredService<ILoggerFactory>() sp.GetRequiredService<ILoggerFactory>()
)); ));
// AutoTellTabsService pulls MessageStore through MessageManager.Store // MessageStore is allocated inside MessageManager.ctor; a separate
// because MessageStore is still allocated inside MessageManager.ctor // container singleton would double-construct the SQLite handle.
// (DI-2a leaves that body untouched). Promoting MessageStore to its
// own container singleton would double-construct the SQLite handle.
services.AddSingleton(sp => services.AddSingleton(sp =>
{ {
var pluginRef = sp.GetRequiredService<Plugin>(); var pluginRef = sp.GetRequiredService<Plugin>();
@@ -149,11 +126,8 @@ internal static class PluginHostFactory
); );
}); });
// ----------------------------------------------------------------- // Block C — Windows. WindowSystem.AddWindow is called from
// Block C — Windows (8, each takes Plugin or ChatLogWindow). The // PluginLifecycle.LoadAsync on the framework thread.
// host never AddWindow()s them; PluginLifecycle does that on the
// framework thread once C3 wires it up (see plan §2 service order).
// -----------------------------------------------------------------
services.AddSingleton(sp => new ChatLogWindow( services.AddSingleton(sp => new ChatLogWindow(
sp.GetRequiredService<Plugin>(), sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<ILogger<ChatLogWindow>>(), sp.GetRequiredService<ILogger<ChatLogWindow>>(),
@@ -173,18 +147,8 @@ internal static class PluginHostFactory
services.AddSingleton(sp => new DebuggerWindow(sp.GetRequiredService<Plugin>())); services.AddSingleton(sp => new DebuggerWindow(sp.GetRequiredService<Plugin>()));
services.AddSingleton(sp => new FirstRunWizard(sp.GetRequiredService<Plugin>())); services.AddSingleton(sp => new FirstRunWizard(sp.GetRequiredService<Plugin>()));
// ----------------------------------------------------------------- // Hosted-service adapters: thin wrappers around the existing init
// Hosted-service adapters — IHostedService is the host's only "after // methods so the service class bodies stay unchanged.
// 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<T>() overload's ActivatorUtilities fails.
services.AddHostedService(sp => new FontManagerInitHostedService( services.AddHostedService(sp => new FontManagerInitHostedService(
sp.GetRequiredService<FontManager>() sp.GetRequiredService<FontManager>()
)); ));
+3 -5
View File
@@ -4,11 +4,9 @@ using Microsoft.Extensions.Hosting;
namespace HellionChat; namespace HellionChat;
// Orchestrates Host.StartAsync / StopAsync + dispose on the framework thread. // Orchestrates Host.StartAsync / StopAsync and the framework-thread dispose.
// The Host itself is built sync in Plugin.ctor (before the schema gate clears) // Plugin.ctor builds the host and assigns it via the Host property, so
// and assigned via the property setter; PluginLifecycle never builds it // PluginLifecycle never constructs the host itself.
// itself, which is why HellionChat skips Lightless' Func-delegate indirection
// (see plan §9 risk "Bewusste Abweichung von Lightless").
internal sealed class PluginLifecycle : IAsyncDisposable internal sealed class PluginLifecycle : IAsyncDisposable
{ {
private readonly IFramework _framework; private readonly IFramework _framework;
+4 -4
View File
@@ -2,10 +2,10 @@ using System;
namespace HellionChat.Util; namespace HellionChat.Util;
// Indirection over Dalamud's IPluginLog so MessageStore can be constructed // Plugin.LogProxy bridge for consumers that cannot take a logger via the
// in an isolated xUnit AppDomain without loading Dalamud.dll — same pattern // constructor: static helpers (EmoteCache et al.), Dalamud-reflected types
// as IPlatformUtil from F12.1. A later DI-container cycle (v1.5.x) may // (Configuration), data classes with mass instantiation (Message) and
// replace this with Microsoft.Extensions.Logging's ILogger<T>. // instance classes that only log from static methods (FontManager).
internal interface IPluginLogProxy internal interface IPluginLogProxy
{ {
void Verbose(string message); void Verbose(string message);