feat(render): implement Campaign AR and terrain fidelity

This commit is contained in:
Erik 2026-08-22 13:13:29 +02:00
parent 99cf26e00c
commit 7a5f96ede5
368 changed files with 50611 additions and 950 deletions

View file

@ -1,4 +1,5 @@
using AcDream.Plugin.Abstractions;
using AcDream.Plugin.Abstractions.Rendering;
namespace AcDream.Core.Plugins;
@ -17,16 +18,25 @@ public readonly record struct PluginSessionStatus(
PluginSessionStatusKind Kind,
string? Error = null);
/// <summary>A configured plugin declares no entry point usable by this host.</summary>
public sealed class PluginHostKindException : Exception
{
public PluginHostKindException(string message) : base(message) { }
}
/// <summary>
/// One host/session-scoped plugin lifetime. Discovery, allow-listing,
/// initialize/enable, failure isolation, reverse-order disable, and collectible
/// load-context release are shared by graphical and no-window hosts so their
/// configured plugin-set semantics cannot drift.
/// initialize/enable, declarative render-pack registration, failure isolation,
/// reverse-order disable, and collectible load-context release are shared by
/// graphical and no-window hosts so their configured plugin-set semantics
/// cannot drift. Unsupported kinds are filtered before assembly loading.
/// </summary>
public sealed class PluginSession : IDisposable
{
private readonly IPluginHost _host;
private readonly Action<PluginSessionStatus>? _report;
private readonly IRenderPackRegistry? _renderPacks;
private readonly HashSet<PluginKind> _supportedKinds;
private readonly List<ActivePlugin> _loaded = [];
private readonly List<WeakReference> _releasedContexts = [];
private bool _started;
@ -34,10 +44,28 @@ public sealed class PluginSession : IDisposable
public PluginSession(
IPluginHost host,
Action<PluginSessionStatus>? report = null)
Action<PluginSessionStatus>? report = null,
IRenderPackRegistry? renderPacks = null,
IEnumerable<PluginKind>? supportedKinds = null)
{
_host = host ?? throw new ArgumentNullException(nameof(host));
_report = report;
_renderPacks = renderPacks;
_supportedKinds = new HashSet<PluginKind>(
supportedKinds
?? (renderPacks is null
? [PluginKind.Gameplay]
: [PluginKind.Gameplay, PluginKind.RenderPack]));
if (_supportedKinds.Count == 0)
throw new ArgumentException(
"At least one supported plugin kind is required.",
nameof(supportedKinds));
if (_supportedKinds.Contains(PluginKind.RenderPack) && renderPacks is null)
{
throw new ArgumentException(
"A host that supports render-pack plugins must supply a render-pack registry.",
nameof(renderPacks));
}
}
public int LoadedCount => _loaded.Count;
@ -122,6 +150,25 @@ public sealed class PluginSession : IDisposable
string id = result.Manifest!.Id;
if (requestedSet is not null && !requestedSet.Contains(id))
continue;
if (!result.Manifest.Kinds.Any(_supportedKinds.Contains))
{
// A shared plugin root may contain graphical-only packs.
// An unfiltered/headless scan omits those silently. An
// explicitly requested id gets one precise host-kind
// failure, still without loading its assembly.
if (requestedSet is not null)
{
AddOrdered(discoveredOrder, id);
AddError(
errors,
id,
new PluginHostKindException(
$"plugin '{id}' declares only "
+ $"{string.Join(", ", result.Manifest.Kinds)} entry points, "
+ "which this host does not support."));
}
continue;
}
AddOrdered(discoveredOrder, id);
if (!candidates.TryGetValue(id, out List<PluginDiscoveryResult>? list))
{
@ -160,23 +207,27 @@ public sealed class PluginSession : IDisposable
{
ActivePlugin active = _loaded[index];
LoadedPlugin loaded = active.Loaded;
try
if (loaded.Plugin is not null)
{
loaded.Plugin!.Disable();
}
catch (Exception error)
{
SafeLog(
static (log, message, exception) =>
log.Error(message, exception),
$"plugin disable failed: {loaded.Manifest.Id}",
error);
try
{
loaded.Plugin.Disable();
}
catch (Exception error)
{
SafeLog(
static (log, message, exception) =>
log.Error(message, exception),
$"plugin disable failed: {loaded.Manifest.Id}",
error);
}
}
// Host-owned registrations are released even when Disable throws.
// This must precede ALC unload so no UI binding or event delegate
// can keep the plugin assembly reachable.
active.Scope.Dispose();
ReleaseRenderScope(active.RenderPackScope, loaded.Manifest.Id);
try
{
@ -208,16 +259,23 @@ public sealed class PluginSession : IDisposable
foreach (PluginDiscoveryResult candidate in available)
{
var scope = new ScopedPluginHost(_host);
ScopedRenderPackRegistry? renderPackScope =
candidate.Manifest!.Declares(PluginKind.RenderPack)
&& _renderPacks is not null
? new ScopedRenderPackRegistry(_renderPacks)
: null;
LoadedPlugin loaded = PluginLoader.Load(
candidate.PluginDirectory,
candidate.Manifest!,
scope);
candidate.Manifest,
scope,
renderPackScope);
if (!loaded.Success)
{
// Initialize can register callbacks before it fails. The
// registration transaction closes before plugin cleanup
// and, critically, before any ALC Unloading notification.
scope.Dispose();
ReleaseRenderScope(renderPackScope, candidate.Manifest.Id);
ReleaseFailedLoad(loaded);
AddError(
errors,
@ -229,8 +287,8 @@ public sealed class PluginSession : IDisposable
try
{
loaded.Plugin!.Enable();
_loaded.Add(new ActivePlugin(loaded, scope));
loaded.Plugin?.Enable();
_loaded.Add(new ActivePlugin(loaded, scope, renderPackScope));
SafeLog(
static (log, message, _) => log.Info(message),
$"plugin loaded: {loaded.Manifest.Id} "
@ -244,7 +302,7 @@ public sealed class PluginSession : IDisposable
catch (Exception error)
{
AddError(errors, id, error);
ReleaseFailedEnable(loaded, scope);
ReleaseFailedEnable(loaded, scope, renderPackScope);
}
}
}
@ -274,22 +332,27 @@ public sealed class PluginSession : IDisposable
private void ReleaseFailedEnable(
LoadedPlugin loaded,
ScopedPluginHost scope)
ScopedPluginHost scope,
ScopedRenderPackRegistry? renderPackScope)
{
try
if (loaded.Plugin is not null)
{
loaded.Plugin!.Disable();
}
catch (Exception error)
{
SafeLog(
static (log, message, exception) =>
log.Error(message, exception),
$"plugin cleanup after enable failure failed: {loaded.Manifest.Id}",
error);
try
{
loaded.Plugin.Disable();
}
catch (Exception error)
{
SafeLog(
static (log, message, exception) =>
log.Error(message, exception),
$"plugin cleanup after enable failure failed: {loaded.Manifest.Id}",
error);
}
}
scope.Dispose();
ReleaseRenderScope(renderPackScope, loaded.Manifest.Id);
_releasedContexts.Add(new WeakReference(loaded.LoadContext!));
try
@ -342,6 +405,26 @@ public sealed class PluginSession : IDisposable
}
}
private void ReleaseRenderScope(
ScopedRenderPackRegistry? scope,
string pluginId)
{
if (scope is null)
return;
try
{
scope.Dispose();
}
catch (Exception error)
{
SafeLog(
static (log, message, exception) =>
log.Error(message, exception),
$"render-pack registration cleanup failed: {pluginId}",
error);
}
}
private void Report(PluginSessionStatus status)
{
if (_report is null)
@ -417,5 +500,6 @@ public sealed class PluginSession : IDisposable
private sealed record ActivePlugin(
LoadedPlugin Loaded,
ScopedPluginHost Scope);
ScopedPluginHost Scope,
ScopedRenderPackRegistry? RenderPackScope);
}