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;
// 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}), "
+1 -4
View File
@@ -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;
}
+4 -1
View File
@@ -16,7 +16,10 @@
<PackageReference Include="MessagePack" Version="[3.1.4, 4.0.0)" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="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.Logging" 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;
// 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
{
@@ -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}");
@@ -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<string, DalamudLogger> _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 =
+10 -20
View File
@@ -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())
+11 -47
View File
@@ -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<PluginLifecycle>() immediately after Build.
services.AddSingleton<PluginLifecycle>();
// -----------------------------------------------------------------
// 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<IPlatformUtil>(_ => new DalamudPlatformUtil());
services.AddSingleton<IPluginLogProxy>(sp => new DalamudPluginLogProxy(
sp.GetRequiredService<IPluginLog>()
@@ -133,10 +112,8 @@ internal static class PluginHostFactory
sp.GetRequiredService<ILoggerFactory>()
));
// 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<Plugin>();
@@ -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<Plugin>(),
sp.GetRequiredService<ILogger<ChatLogWindow>>(),
@@ -173,18 +147,8 @@ internal static class PluginHostFactory
services.AddSingleton(sp => new DebuggerWindow(sp.GetRequiredService<Plugin>()));
services.AddSingleton(sp => new FirstRunWizard(sp.GetRequiredService<Plugin>()));
// -----------------------------------------------------------------
// 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<T>() 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<FontManager>()
));
+3 -5
View File
@@ -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;
+4 -4
View File
@@ -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<T>.
// 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);