acdream/tests/AcDream.Plugin.Tests.Fixtures.HostPlugin/HostPlugin.cs

188 lines
6.6 KiB
C#

using AcDream.Plugin.Abstractions;
using AcDream.Plugin.Abstractions.Rendering;
using System.Runtime.Loader;
namespace AcDream.Plugin.Tests.Fixtures.HostPlugin;
/// <summary>
/// Cross-host LA5 fixture. It deliberately takes the same path on graphical
/// and no-window hosts: observe the capability, register UI, and subscribe to
/// gameplay events. A headless registry must make the UI call harmless without
/// retaining this instance in the default load context.
/// </summary>
public sealed class HostPlugin : IAcDreamPlugin, IRenderPackPlugin, IRenderPackAssets
{
private IPluginHost? _host;
private string? _assemblyDirectory;
private bool _throwAfterRegistration;
private bool _throwDuringInitialize;
private int _entitiesSeen;
public void Initialize(IPluginHost host)
{
_host = host ?? throw new ArgumentNullException(nameof(host));
_assemblyDirectory = Path.GetDirectoryName(
typeof(HostPlugin).Assembly.Location);
_throwAfterRegistration = File.Exists(
Path.Combine(_assemblyDirectory!, "throw-after-register"));
_throwDuringInitialize = File.Exists(
Path.Combine(_assemblyDirectory!, "throw-during-initialize"));
host.Log.Info($"fixture-initialized:hasUi={host.HasUi}");
if (_throwDuringInitialize)
{
RegisterHostCallbacks(host);
AssemblyLoadContext.GetLoadContext(typeof(HostPlugin).Assembly)!
.Unloading += OnUnloading;
throw new InvalidOperationException(
"fixture initialize failed after registering UI, entity, and selection callbacks");
}
}
public void Enable()
{
IPluginHost host = _host
?? throw new InvalidOperationException("The fixture was not initialized.");
RegisterHostCallbacks(host);
if (_throwAfterRegistration)
{
throw new InvalidOperationException(
"fixture enable failed after registering UI and events");
}
host.Log.Info(
$"fixture-enabled:hasUi={host.HasUi}:entities={host.State.Entities.Count}");
}
public void Disable()
{
IPluginHost? host = _host;
if (host is null)
return;
if (_throwAfterRegistration || _throwDuringInitialize)
{
throw new InvalidOperationException(
"fixture disable intentionally refuses cleanup");
}
host.Events.EntitySpawned -= OnEntitySpawned;
host.Selection.Changed -= OnSelectionChanged;
host.Log.Info($"fixture-disabled:entitiesSeen={_entitiesSeen}");
_host = null;
}
public void Register(IRenderPackRegistry registry)
{
ArgumentNullException.ThrowIfNull(registry);
string directory = Path.GetDirectoryName(
typeof(HostPlugin).Assembly.Location)!;
if (File.Exists(Path.Combine(directory, "register-no-render-packs")))
return;
string versionPath = Path.Combine(directory, "render-pack-version.txt");
Version version = File.Exists(versionPath)
? Version.Parse(File.ReadAllText(versionPath).Trim())
: new Version(1, 0, 0);
_ = registry.Register(
new RenderPackDescriptor(
Id: "acdream.test.external-render-pack",
DisplayName: "External graphical render-pack fixture",
PackVersion: version,
PackApiVersion: RenderPackApi.Current,
HighestTier: RenderPackTier.Tier1,
RequiredCapabilities: [],
OptionalCapabilities: [],
Resources: [],
Passes: [],
SceneReplays: [],
PipelineVariants: [],
QualityPresets:
[
Preset("low", "Low"),
Preset("high", "High"),
],
Settings: [],
AtmospherePolicy: null)
{
FeatureSummary = "Collectible external no-op render-pack fixture.",
},
this);
if (File.Exists(Path.Combine(directory, "throw-after-render-pack-register")))
{
throw new InvalidOperationException(
"fixture render-pack registration failed after publishing its descriptor");
}
}
public Stream OpenRead(string assetKey) =>
new MemoryStream([], writable: false);
private static RenderQualityPreset Preset(string id, string displayName) => new(
id,
displayName,
RequiredCapabilities: [],
ResourceOverrides: [],
SettingOverrides: [],
MaxResidentGpuBytes: 0,
MaxIncrementalGpuMillisecondsP50: 0,
MaxIncrementalGpuMillisecondsP99: 0,
MaxIncrementalCpuMillisecondsP50: 0,
MaxIncrementalCpuMillisecondsP99: 0);
private void RegisterHostCallbacks(IPluginHost host)
{
host.Ui.AddMarkupPanel(
Path.Combine(AppContext.BaseDirectory, "fixture-panel.xml"),
this);
host.Events.EntitySpawned += OnEntitySpawned;
host.Selection.Changed += OnSelectionChanged;
}
private void OnEntitySpawned(WorldEntitySnapshot snapshot)
{
_entitiesSeen++;
RecordUnexpectedCallback(snapshot.Id);
}
private void OnSelectionChanged(SelectionChangedEvent change) =>
RecordUnexpectedCallback(change.SelectedObjectId ?? 0u);
private void RecordUnexpectedCallback(uint objectId)
{
if ((_throwAfterRegistration || _throwDuringInitialize)
&& _assemblyDirectory is not null)
{
File.AppendAllText(
Path.Combine(_assemblyDirectory, "unexpected-callback"),
$"{objectId}{Environment.NewLine}");
}
}
private void OnUnloading(AssemblyLoadContext context)
{
IPluginHost host = _host!;
bool uiClosed = Rejects(() => host.Ui.AddMarkupPanel(
Path.Combine(AppContext.BaseDirectory, "unloading-panel.xml"),
this));
bool eventsClosed = Rejects(() =>
{
host.Events.EntitySpawned += OnEntitySpawned;
});
bool selectionClosed = Rejects(() =>
{
host.Selection.Changed += OnSelectionChanged;
});
File.WriteAllText(
Path.Combine(_assemblyDirectory!, "unload-observation"),
$"ui={uiClosed};events={eventsClosed};selection={selectionClosed}");
}
private static bool Rejects(Action action)
{
try
{
action();
return false;
}
catch (ObjectDisposedException)
{
return true;
}
}
}