feat(app): wire plugin host, ship smoke plugin, log lifecycle
Phase 1 MVP end-to-end. Program.cs initializes Serilog, builds an AppPluginHost that hands plugins a SerilogAdapter (IPluginLogger), discovers plugins from the App's output plugins/ dir, loads each via PluginLoader, calls Enable on all of them before opening the GameWindow, and calls Disable in a finally block on shutdown. AcDream.Plugins.Smoke is a new first-party plugin that logs through the host during Initialize / Enable / Disable. Its csproj references the abstractions with Private=false + ExcludeAssets=runtime to avoid shipping a second copy of AcDream.Plugin.Abstractions.dll (which would break ALC type identity). An MSBuild Target on the App project copies the plugin DLL into plugins/AcDream.Plugins.Smoke/ and writes the plugin.json manifest next to it. Smoke verified against real dats. Console output observed: [INF] scanning plugins in ...\plugins [INF] smoke plugin initialized [INF] loaded plugin acdream.smoke (Smoke Plugin) [INF] smoke plugin enabled loaded landblock 0xA9B4FFFF <window renders terrain> [INF] smoke plugin disabled (on shutdown) Phase 1 done. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
87c45c70ac
commit
fb83e0bb6f
7 changed files with 130 additions and 6 deletions
|
|
@ -4,6 +4,7 @@
|
|||
<Project Path="src/AcDream.Cli/AcDream.Cli.csproj" />
|
||||
<Project Path="src/AcDream.Core/AcDream.Core.csproj" />
|
||||
<Project Path="src/AcDream.Plugin.Abstractions/AcDream.Plugin.Abstractions.csproj" />
|
||||
<Project Path="src/AcDream.Plugins.Smoke/AcDream.Plugins.Smoke.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tests/">
|
||||
<Project Path="tests/AcDream.Core.Tests.Fixtures.HelloPlugin/AcDream.Core.Tests.Fixtures.HelloPlugin.csproj" />
|
||||
|
|
|
|||
|
|
@ -24,4 +24,26 @@
|
|||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<!-- Build the smoke plugin first and copy it into plugins/AcDream.Plugins.Smoke/ -->
|
||||
<ProjectReference Include="..\AcDream.Plugins.Smoke\AcDream.Plugins.Smoke.csproj">
|
||||
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
|
||||
<SkipGetTargetFrameworkProperties>true</SkipGetTargetFrameworkProperties>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Target Name="CopySmokePluginToPluginsDir" AfterTargets="Build">
|
||||
<PropertyGroup>
|
||||
<_SmokePluginSourceDir>..\AcDream.Plugins.Smoke\bin\$(Configuration)\net10.0</_SmokePluginSourceDir>
|
||||
<_SmokePluginDestDir>$(OutputPath)plugins\AcDream.Plugins.Smoke</_SmokePluginDestDir>
|
||||
</PropertyGroup>
|
||||
<MakeDir Directories="$(_SmokePluginDestDir)" />
|
||||
<Copy
|
||||
SourceFiles="$(_SmokePluginSourceDir)\AcDream.Plugins.Smoke.dll"
|
||||
DestinationFolder="$(_SmokePluginDestDir)"
|
||||
SkipUnchangedFiles="true" />
|
||||
<WriteLinesToFile
|
||||
File="$(_SmokePluginDestDir)\plugin.json"
|
||||
Overwrite="true"
|
||||
Lines="{ "id": "acdream.smoke", "displayName": "Smoke Plugin", "version": "0.1.0", "entryDll": "AcDream.Plugins.Smoke.dll", "apiVersion": 1 }" />
|
||||
</Target>
|
||||
</Project>
|
||||
|
|
|
|||
9
src/AcDream.App/Plugins/AppPluginHost.cs
Normal file
9
src/AcDream.App/Plugins/AppPluginHost.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
using AcDream.Plugin.Abstractions;
|
||||
|
||||
namespace AcDream.App.Plugins;
|
||||
|
||||
public sealed class AppPluginHost : IPluginHost
|
||||
{
|
||||
public AppPluginHost(IPluginLogger log) => Log = log;
|
||||
public IPluginLogger Log { get; }
|
||||
}
|
||||
12
src/AcDream.App/Plugins/SerilogAdapter.cs
Normal file
12
src/AcDream.App/Plugins/SerilogAdapter.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
using AcDream.Plugin.Abstractions;
|
||||
|
||||
namespace AcDream.App.Plugins;
|
||||
|
||||
public sealed class SerilogAdapter : IPluginLogger
|
||||
{
|
||||
private readonly Serilog.ILogger _log;
|
||||
public SerilogAdapter(Serilog.ILogger log) => _log = log;
|
||||
public void Info(string message) => _log.Information("{Message}", message);
|
||||
public void Warn(string message) => _log.Warning("{Message}", message);
|
||||
public void Error(string message, Exception? exception = null) => _log.Error(exception, "{Message}", message);
|
||||
}
|
||||
|
|
@ -1,15 +1,61 @@
|
|||
using AcDream.App.Plugins;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.Core.Plugins;
|
||||
using Serilog;
|
||||
|
||||
var datDir = args.FirstOrDefault()
|
||||
?? Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR");
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Debug()
|
||||
.WriteTo.Console()
|
||||
.CreateLogger();
|
||||
|
||||
var datDir = args.FirstOrDefault() ?? Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR");
|
||||
if (string.IsNullOrWhiteSpace(datDir))
|
||||
{
|
||||
Console.Error.WriteLine("usage: AcDream.App <dat-directory>");
|
||||
Console.Error.WriteLine(" or: set ACDREAM_DAT_DIR and run with no args");
|
||||
Log.Error("usage: AcDream.App <dat-directory> (or set ACDREAM_DAT_DIR)");
|
||||
return 2;
|
||||
}
|
||||
|
||||
using var window = new GameWindow(datDir);
|
||||
window.Run();
|
||||
var host = new AppPluginHost(new SerilogAdapter(Log.Logger));
|
||||
|
||||
var pluginsDir = Path.Combine(AppContext.BaseDirectory, "plugins");
|
||||
Log.Information("scanning plugins in {PluginsDir}", pluginsDir);
|
||||
|
||||
var loaded = new List<LoadedPlugin>();
|
||||
foreach (var result in PluginDiscovery.Scan(pluginsDir))
|
||||
{
|
||||
if (!result.Success)
|
||||
{
|
||||
Log.Warning("plugin discovery failed for {Dir}: {Error}", result.PluginDirectory, result.Error);
|
||||
continue;
|
||||
}
|
||||
|
||||
var loadResult = PluginLoader.Load(result.PluginDirectory, result.Manifest!, host);
|
||||
if (!loadResult.Success)
|
||||
{
|
||||
Log.Warning("plugin load failed for {Id}: {Error}", result.Manifest!.Id, loadResult.Error);
|
||||
continue;
|
||||
}
|
||||
|
||||
loaded.Add(loadResult);
|
||||
Log.Information("loaded plugin {Id} ({DisplayName})", result.Manifest!.Id, result.Manifest.DisplayName);
|
||||
}
|
||||
|
||||
foreach (var plugin in loaded)
|
||||
plugin.Plugin!.Enable();
|
||||
|
||||
try
|
||||
{
|
||||
using var window = new GameWindow(datDir);
|
||||
window.Run();
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var plugin in loaded)
|
||||
{
|
||||
try { plugin.Plugin!.Disable(); }
|
||||
catch (Exception ex) { Log.Error(ex, "plugin disable failed: {Id}", plugin.Manifest.Id); }
|
||||
}
|
||||
Log.CloseAndFlush();
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
|
|
|||
17
src/AcDream.Plugins.Smoke/AcDream.Plugins.Smoke.csproj
Normal file
17
src/AcDream.Plugins.Smoke/AcDream.Plugins.Smoke.csproj
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<!-- Plugin DLLs are copied to plugins/<id>/ at build of AcDream.App.
|
||||
They must NOT bring AcDream.Plugin.Abstractions.dll with them;
|
||||
the host already owns it. -->
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AcDream.Plugin.Abstractions\AcDream.Plugin.Abstractions.csproj">
|
||||
<Private>false</Private>
|
||||
<ExcludeAssets>runtime</ExcludeAssets>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
17
src/AcDream.Plugins.Smoke/SmokePlugin.cs
Normal file
17
src/AcDream.Plugins.Smoke/SmokePlugin.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
using AcDream.Plugin.Abstractions;
|
||||
|
||||
namespace AcDream.Plugins.Smoke;
|
||||
|
||||
public sealed class SmokePlugin : IAcDreamPlugin
|
||||
{
|
||||
private IPluginHost? _host;
|
||||
|
||||
public void Initialize(IPluginHost host)
|
||||
{
|
||||
_host = host;
|
||||
_host.Log.Info("smoke plugin initialized");
|
||||
}
|
||||
|
||||
public void Enable() => _host?.Log.Info("smoke plugin enabled");
|
||||
public void Disable() => _host?.Log.Info("smoke plugin disabled");
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue