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,5 +1,6 @@
using System.Reflection;
using AcDream.Plugin.Abstractions;
using AcDream.Plugin.Abstractions.Rendering;
namespace AcDream.Core.Plugins;
@ -7,14 +8,19 @@ public static class PluginLoader
{
/// <summary>
/// Load a plugin DLL from <paramref name="pluginDirectory"/> into a collectible
/// <see cref="System.Runtime.Loader.AssemblyLoadContext"/>, find the first type
/// implementing <see cref="IAcDreamPlugin"/>, instantiate it, and call its
/// <see cref="IAcDreamPlugin.Initialize"/> with the supplied host. Any failure
/// is returned as a failed <see cref="LoadedPlugin"/> rather than thrown.
/// <see cref="System.Runtime.Loader.AssemblyLoadContext"/>, resolve the gameplay
/// and/or render-pack entry points declared by its manifest, and invoke only
/// the facilities this host supplied. Any failure is returned as a failed
/// <see cref="LoadedPlugin"/> rather than thrown.
/// A returned partial plugin/context remains caller-owned; this method never
/// requests unload because the caller must close host registrations first.
/// requests unload because the caller must close host and render-pack
/// registrations first.
/// </summary>
public static LoadedPlugin Load(string pluginDirectory, PluginManifest manifest, IPluginHost host)
public static LoadedPlugin Load(
string pluginDirectory,
PluginManifest manifest,
IPluginHost host,
IRenderPackRegistry? renderPacks = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(pluginDirectory);
ArgumentNullException.ThrowIfNull(manifest);
@ -45,6 +51,7 @@ public static class PluginLoader
PluginAssemblyLoadContext? alc = null;
IAcDreamPlugin? instance = null;
IRenderPackPlugin? renderPackInstance = null;
try
{
alc = new PluginAssemblyLoadContext(pluginDirectory, dllPath);
@ -60,10 +67,51 @@ public static class PluginLoader
types = rtle.Types.OfType<Type>();
}
var pluginType = types
.FirstOrDefault(t => !t.IsAbstract && typeof(IAcDreamPlugin).IsAssignableFrom(t));
Type[] concreteTypes = types
.Where(static type => !type.IsAbstract && !type.IsInterface)
.ToArray();
Type? pluginType = manifest.Declares(PluginKind.Gameplay)
? concreteTypes.FirstOrDefault(
static type => typeof(IAcDreamPlugin).IsAssignableFrom(type))
: null;
bool registerRenderPack =
manifest.Declares(PluginKind.RenderPack) && renderPacks is not null;
CountingRenderPackRegistry? countedRenderPacks = registerRenderPack
? new CountingRenderPackRegistry(renderPacks!)
: null;
Type? renderPackType = null;
if (registerRenderPack)
{
Type[] renderPackTypes = concreteTypes
.Where(static type => typeof(IRenderPackPlugin).IsAssignableFrom(type))
.ToArray();
if (renderPackTypes.Length != 1)
{
return new LoadedPlugin(
manifest,
Plugin: null,
LoadContext: alc,
Error: new InvalidOperationException(
$"render-pack entry DLL '{manifest.EntryDll}' must contain exactly "
+ "one IRenderPackPlugin implementation; found "
+ renderPackTypes.Length));
}
if (pluginType is null)
renderPackType = renderPackTypes[0];
if (!renderPackType.IsVisible
|| renderPackType.GetConstructor(Type.EmptyTypes) is null)
{
return new LoadedPlugin(
manifest,
Plugin: null,
LoadContext: alc,
Error: new InvalidOperationException(
$"render-pack entry type '{renderPackType.FullName}' must be public, "
+ "non-abstract, and expose a public parameterless constructor"));
}
}
if (manifest.Declares(PluginKind.Gameplay) && pluginType is null)
{
return new LoadedPlugin(
manifest,
@ -73,9 +121,58 @@ public static class PluginLoader
$"no IAcDreamPlugin implementation found in {manifest.EntryDll}"));
}
instance = (IAcDreamPlugin)Activator.CreateInstance(pluginType)!;
instance.Initialize(host);
return new LoadedPlugin(manifest, instance, alc, Error: null);
if (registerRenderPack && renderPackType is null)
{
return new LoadedPlugin(
manifest,
Plugin: null,
LoadContext: alc,
Error: new InvalidOperationException(
$"no IRenderPackPlugin implementation found in {manifest.EntryDll}"));
}
if (pluginType is null && renderPackType is null)
{
return new LoadedPlugin(
manifest,
Plugin: null,
LoadContext: alc,
Error: new InvalidOperationException(
"the host did not supply any facility declared by this plugin"));
}
object? sharedInstance = null;
if (pluginType is not null)
{
sharedInstance = Activator.CreateInstance(pluginType);
instance = (IAcDreamPlugin?)sharedInstance
?? throw new InvalidOperationException(
$"could not construct IAcDreamPlugin {pluginType.FullName}");
instance.Initialize(host);
}
if (renderPackType is not null)
{
object renderObject = ReferenceEquals(renderPackType, pluginType)
? sharedInstance!
: Activator.CreateInstance(renderPackType)
?? throw new InvalidOperationException(
$"could not construct IRenderPackPlugin {renderPackType.FullName}");
renderPackInstance = (IRenderPackPlugin)renderObject;
renderPackInstance.Register(countedRenderPacks!);
if (countedRenderPacks!.RegistrationCount == 0)
{
throw new InvalidOperationException(
$"render-pack entry point '{renderPackType.FullName}' registered no packs");
}
}
return new LoadedPlugin(
manifest,
instance,
alc,
Error: null,
renderPackInstance);
}
catch (Exception ex)
{
@ -87,7 +184,45 @@ public static class PluginLoader
manifest,
Plugin: instance,
LoadContext: alc,
Error: ex);
Error: ex,
RenderPackPlugin: renderPackInstance);
}
}
private sealed class CountingRenderPackRegistry(IRenderPackRegistry inner) :
IRenderPackRegistry
{
private int _registrationCount;
internal int RegistrationCount => Volatile.Read(ref _registrationCount);
public IDisposable Register(
RenderPackDescriptor descriptor,
IRenderPackAssets assets)
{
IDisposable registration = inner.Register(descriptor, assets)
?? throw new InvalidOperationException(
"The render-pack registry returned a null registration handle.");
Interlocked.Increment(ref _registrationCount);
return new CountedRegistration(this, registration);
}
private sealed class CountedRegistration(
CountingRenderPackRegistry owner,
IDisposable inner) : IDisposable
{
private CountingRenderPackRegistry? _owner = owner;
private IDisposable? _inner = inner;
public void Dispose()
{
CountingRenderPackRegistry? activeOwner =
Interlocked.Exchange(ref _owner, null);
if (activeOwner is null)
return;
Interlocked.Decrement(ref activeOwner._registrationCount);
Interlocked.Exchange(ref _inner, null)?.Dispose();
}
}
}
}