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

@ -2,6 +2,7 @@ using System.Text.Json;
using AcDream.Core.Plugins;
using AcDream.Core.Selection;
using AcDream.Plugin.Abstractions;
using AcDream.Plugin.Abstractions.Rendering;
namespace AcDream.Core.Tests.Plugins;
@ -73,11 +74,118 @@ public sealed class PluginSessionTests
Assert.Empty(statuses);
}
[Fact]
public void GraphicalKindSet_RegistersRenderPackAndWithdrawsBeforeUnload()
{
using var temporary = new TemporaryDirectory();
InstallFixture(
temporary.Path,
"render",
"acdream.test.render",
[PluginKind.RenderPack]);
var statuses = new List<PluginSessionStatus>();
var registry = new RecordingRenderPackRegistry();
var plugins = new PluginSession(
new StubHost(),
statuses.Add,
registry,
[PluginKind.Gameplay, PluginKind.RenderPack]);
plugins.Start([temporary.Path], allowList: null);
Assert.Equal(1, plugins.LoadedCount);
Assert.Equal(1, registry.ActiveCount);
Assert.Equal("acdream.test.noop-pack", registry.Descriptor?.Id);
IReadOnlyList<WeakReference> contexts =
plugins.CaptureLoadContextWeakReferences();
plugins.Dispose();
Assert.Equal(0, registry.ActiveCount);
Collect(contexts);
}
[Fact]
public void GameplayOnlyHost_SkipsUnrequestedRenderPackBeforeDllProbe()
{
using var temporary = new TemporaryDirectory();
InstallBroken(
temporary.Path,
"render",
"acdream.test.render",
[PluginKind.RenderPack]);
var statuses = new List<PluginSessionStatus>();
using var plugins = new PluginSession(new StubHost(), statuses.Add);
plugins.Start([temporary.Path], allowList: null);
Assert.Equal(0, plugins.LoadedCount);
Assert.Empty(statuses);
Assert.Empty(plugins.CaptureLoadContextWeakReferences());
}
[Fact]
public void RenderPackRegisterFailure_WithdrawsPartialRegistrationBeforeUnload()
{
using var temporary = new TemporaryDirectory();
InstallFixture(
temporary.Path,
"render",
"acdream.test.render",
[PluginKind.RenderPack]);
File.WriteAllText(
Path.Combine(temporary.Path, "render", "throw-after-render-register"),
string.Empty);
var registry = new RecordingRenderPackRegistry();
var statuses = new List<PluginSessionStatus>();
var plugins = new PluginSession(
new StubHost(),
statuses.Add,
registry,
[PluginKind.Gameplay, PluginKind.RenderPack]);
plugins.Start([temporary.Path], allowList: null);
Assert.Equal(0, plugins.LoadedCount);
Assert.Equal(0, registry.ActiveCount);
PluginSessionStatus status = Assert.Single(statuses);
Assert.Equal(PluginSessionStatusKind.Failed, status.Kind);
Assert.Contains("failed after publishing", status.Error);
IReadOnlyList<WeakReference> contexts =
plugins.CaptureLoadContextWeakReferences();
plugins.Dispose();
Collect(contexts);
}
[Fact]
public void GameplayOnlyHost_ExplicitRenderPackReportsKindWithoutDllProbe()
{
using var temporary = new TemporaryDirectory();
InstallBroken(
temporary.Path,
"render",
"acdream.test.render",
[PluginKind.RenderPack]);
var statuses = new List<PluginSessionStatus>();
using var plugins = new PluginSession(new StubHost(), statuses.Add);
plugins.Start([temporary.Path], ["acdream.test.render"]);
PluginSessionStatus status = Assert.Single(statuses);
Assert.Equal(PluginSessionStatusKind.Failed, status.Kind);
Assert.Contains("does not support", status.Error);
Assert.DoesNotContain("entry dll", status.Error);
Assert.Empty(plugins.CaptureLoadContextWeakReferences());
}
private static void ReleaseAndCollect(PluginSession plugins)
{
IReadOnlyList<WeakReference> contexts =
plugins.CaptureLoadContextWeakReferences();
plugins.Dispose();
Collect(contexts);
}
private static void Collect(IReadOnlyList<WeakReference> contexts)
{
for (int attempt = 0;
attempt < 10 && contexts.Any(static context => context.IsAlive);
attempt++)
@ -90,6 +198,13 @@ public sealed class PluginSessionTests
}
private static void InstallFixture(string root, string folder, string id)
=> InstallFixture(root, folder, id, kinds: null);
private static void InstallFixture(
string root,
string folder,
string id,
IReadOnlyList<PluginKind>? kinds)
{
string source = FixturePluginPath();
Assert.True(File.Exists(source), $"fixture DLL not found: {source}");
@ -97,20 +212,25 @@ public sealed class PluginSessionTests
Directory.CreateDirectory(pluginDirectory);
string fileName = Path.GetFileName(source);
File.Copy(source, Path.Combine(pluginDirectory, fileName));
WriteManifest(pluginDirectory, id, fileName);
WriteManifest(pluginDirectory, id, fileName, kinds);
}
private static void InstallBroken(string root, string folder, string id)
private static void InstallBroken(
string root,
string folder,
string id,
IReadOnlyList<PluginKind>? kinds = null)
{
string pluginDirectory = Path.Combine(root, folder);
Directory.CreateDirectory(pluginDirectory);
WriteManifest(pluginDirectory, id, "missing.dll");
WriteManifest(pluginDirectory, id, "missing.dll", kinds);
}
private static void WriteManifest(
string directory,
string id,
string entryDll) =>
string entryDll,
IReadOnlyList<PluginKind>? kinds = null) =>
File.WriteAllText(
Path.Combine(directory, "plugin.json"),
JsonSerializer.Serialize(new
@ -120,8 +240,46 @@ public sealed class PluginSessionTests
version = "1.0.0",
entryDll,
apiVersion = 1,
kinds = kinds?.Select(static kind => kind.ToString()),
}));
private sealed class RecordingRenderPackRegistry : IRenderPackRegistry
{
private readonly List<Registration> _registrations = [];
internal int ActiveCount => _registrations.Count;
internal RenderPackDescriptor? Descriptor { get; private set; }
public IDisposable Register(
RenderPackDescriptor descriptor,
IRenderPackAssets assets)
{
Descriptor = descriptor;
var registration = new Registration(this, assets);
_registrations.Add(registration);
return registration;
}
private void Remove(Registration registration) =>
_registrations.Remove(registration);
private sealed class Registration(
RecordingRenderPackRegistry owner,
IRenderPackAssets assets) : IDisposable
{
private RecordingRenderPackRegistry? _owner = owner;
private IRenderPackAssets? _assets = assets;
public void Dispose()
{
RecordingRenderPackRegistry? current =
Interlocked.Exchange(ref _owner, null);
_assets = null;
current?.Remove(this);
}
}
}
private static string FixturePluginPath()
{
string configuration = new DirectoryInfo(AppContext.BaseDirectory)