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:
@@ -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}), "
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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())
|
||||
|
||||
@@ -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>()
|
||||
));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user