Files
Anvil/Anvil/PluginHostFactory.cs
T
JonKazama-Hellion 90803bcd3c feat(bootstrap): wire DI host, hosted services, and plugin entry
The plugin is now loadable. Dalamud injects five services into the Plugin
constructor (Lightless pattern), the constructor builds the generic-host
container synchronously, and PluginLifecycle drives StartAsync from
LoadAsync. Module 02+ extends PluginHostFactory; this file set stays put.

- PluginHostDependencies (record): bundles the five Dalamud services
  v0.1.0 needs (IDalamudPluginInterface, IPluginLog, IDataManager,
  IFramework, ISelfTestRegistry).
- PluginHostFactory.Build: HostBuilder + AddDalamudLogging + the four
  service blocks (Dalamud services, Anvil singletons, ISelfTestStep
  collection, IHostedService init chain). Every registration uses an
  explicit factory lambda - the default activator only sees public
  ctors and Anvil follows the internal-sealed convention.
- PluginLifecycle (IAsyncDisposable): owns Host.StartAsync, marshals the
  Host.Dispose call onto the framework thread, idempotency guard via
  Interlocked, ExceptionDispatchInfo.Capture preserves the original
  load-throw stack when a failure cascades.
- Plugin (IAsyncDalamudPlugin): constructor injection of the five
  Dalamud services, builds the dependencies record, kicks off the host
  build, hands DisposeAsync to the lifecycle.
- Hosting/RecipeDataLoadHostedService: dispatches LuminaRecipeAdapter
  .LoadInternal onto the framework thread on StartAsync. Lumina sheet
  reads have no documented thread safety; conservative default.
- Hosting/SelfTestRegistrationHostedService: collects every
  ISelfTestStep registration from DI and hands them to
  ISelfTestRegistry.RegisterTestSteps once the host is up.
- SelfTest/RecipeDataAdapterLoadStep: nine pass criteria per spec §4.1
  (IsLoaded, RecipeCount > 0, ActionCount in 30..80, BuffsByKind.Count
  == 14, ConditionsByKind.Count == 11, Foods >= 30, Medicines >= 5,
  BasicSynthesis.RowIdByClassJob[8] == 100001, Cosmic-surface
  silent-degradation warning). Returns Waiting while the catalog is
  still loading.
- Infrastructure/Logging trio: DalamudLogger maps
  Microsoft.Extensions.Logging levels to IPluginLog, the provider
  emits an Anvil bootstrap banner with a Forge-Bronze fingerprint on
  ctor, the extension wires the provider into the ILoggingBuilder via
  TryAddEnumerable.
2026-05-27 21:58:38 +02:00

102 lines
4.1 KiB
C#

// Builds the generic-host DI container for Anvil. v0.1.0 is intentionally
// thin - module 01 (RecipeData) plus the bootstrap glue. Module 02+ will
// extend the Block B / Block C sections, not replace them.
//
// Every service registration goes through a factory lambda. The default
// AddSingleton<T>() overload reflects public constructors, and Anvil
// follows the Goatcorp / Dalamud convention of internal-sealed types -
// the activator would throw on the first resolve otherwise (HellionChat
// v1.5.0 C0 smoke documented this). The lambda compiles inside Anvil's
// namespace where internal access works.
using System.Collections.Generic;
using Anvil.Hosting;
using Anvil.Infrastructure.Logging;
using Anvil.RecipeData;
using Anvil.RecipeData.Internal;
using Anvil.SelfTest;
using Dalamud.Plugin;
using Dalamud.Plugin.SelfTest;
using Dalamud.Plugin.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Anvil;
internal static class PluginHostFactory
{
public static IHost Build(Plugin plugin, PluginHostDependencies dependencies)
{
return new HostBuilder()
.UseContentRoot(dependencies.PluginInterface.ConfigDirectory.FullName)
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.AddDalamudLogging(dependencies.PluginLog);
logging.SetMinimumLevel(LogLevel.Trace);
})
.ConfigureServices(services => ConfigureServices(services, plugin, dependencies))
.Build();
}
private static void ConfigureServices(
IServiceCollection services,
Plugin plugin,
PluginHostDependencies dependencies
)
{
// ---- Block A: Dalamud services ----
services.AddSingleton(dependencies);
services.AddSingleton(dependencies.PluginInterface);
services.AddSingleton(dependencies.PluginLog);
services.AddSingleton(dependencies.DataManager);
services.AddSingleton(dependencies.Framework);
services.AddSingleton(dependencies.SelfTestRegistry);
// Self-reference. Lets services that genuinely need the plugin
// back-ref (later modules; v0.1.0 has none) reach it without a
// Plugin.Instance static.
services.AddSingleton(plugin);
services.AddSingleton<PluginLifecycle>(sp => new PluginLifecycle(
sp.GetRequiredService<IFramework>(),
sp.GetRequiredService<Plugin>()
));
// ---- Block B: Anvil singletons ----
services.AddSingleton<RecipeDataCatalog>(_ => new RecipeDataCatalog());
services.AddSingleton<LuminaRecipeAdapter>(sp => new LuminaRecipeAdapter(
sp.GetRequiredService<IDataManager>(),
sp.GetRequiredService<ILogger<LuminaRecipeAdapter>>(),
sp.GetRequiredService<RecipeDataCatalog>()
));
// ---- Block C: SelfTest steps (collected as IEnumerable<ISelfTestStep>) ----
services.AddSingleton<ISelfTestStep, RecipeDataAdapterLoadStep>(
sp => new RecipeDataAdapterLoadStep(
sp.GetRequiredService<RecipeDataCatalog>(),
sp.GetRequiredService<ILogger<RecipeDataAdapterLoadStep>>()
)
);
// ---- Block D: Hosted services (init order is registration order) ----
// RecipeData adapter runs first so the catalog is populated before
// the self-test registry sees its steps.
services.AddHostedService<RecipeDataLoadHostedService>(
sp => new RecipeDataLoadHostedService(
sp.GetRequiredService<IFramework>(),
sp.GetRequiredService<LuminaRecipeAdapter>(),
sp.GetRequiredService<ILogger<RecipeDataLoadHostedService>>()
)
);
services.AddHostedService<SelfTestRegistrationHostedService>(
sp => new SelfTestRegistrationHostedService(
sp.GetRequiredService<ISelfTestRegistry>(),
sp.GetRequiredService<IEnumerable<ISelfTestStep>>(),
sp.GetRequiredService<ILogger<SelfTestRegistrationHostedService>>()
)
);
}
}